- Implement clone-with functionality with one/two argument support - Add preservation of evaluation order for clone-with operations - Support dynamic, numeric and object-valued properties in clone-with - Handle error cases and stop property updates at first error - Reject active references and unwrap sole remaining references - Respect property scope and unlock readonly properties during cloning - Add first-class callable and string callable support for clone - Preserve lexical class scope in Zend call frames for method calls - Update documentation with clone-with compatibility requirements - Add comprehensive test coverage for clone-with features - Implement scope argument handling in runtime function callsmaster
parent
9fc5755544
commit
795a8e0a1d
13 changed files with 462 additions and 4 deletions
@ -0,0 +1,16 @@ |
||||
<?php |
||||
|
||||
class CloneWithCodegen |
||||
{ |
||||
private int $value = 1; |
||||
|
||||
public function copy(): self |
||||
{ |
||||
return clone($this, ['value' => 2]); |
||||
} |
||||
} |
||||
|
||||
function clone_with_global(CloneWithCodegen $value): CloneWithCodegen |
||||
{ |
||||
return clone($value, []); |
||||
} |
||||
@ -0,0 +1,62 @@ |
||||
<?php |
||||
/** |
||||
* This file is part of Swoole-Compiler(AOT). |
||||
* |
||||
* @link https://www.swoole.com/ |
||||
* @contact service@swoole.com |
||||
*/ |
||||
|
||||
use TypePhp\CompilerTest; |
||||
|
||||
/** |
||||
* @internal |
||||
* @coversNothing |
||||
*/ |
||||
final class CloneWithCodegenTest extends BaseTest |
||||
{ |
||||
public function testMethodCloneWithPassesLexicalScopeToZend(): void |
||||
{ |
||||
$code = $this->compileFixture(); |
||||
$method = $this->functionBody($code, 'php_clonewithcodegen__copy'); |
||||
|
||||
self::assertMatchesRegularExpression( |
||||
'/php::call\(get_persistent_class\([^;]+?, get_(?:persistent_)?func\(/', |
||||
$method, |
||||
); |
||||
} |
||||
|
||||
public function testGlobalCloneWithDoesNotInventClassScope(): void |
||||
{ |
||||
$code = $this->compileFixture(); |
||||
$function = $this->functionBody($code, 'php_clone_with_global'); |
||||
|
||||
self::assertMatchesRegularExpression('/php::call\(get_(?:persistent_)?func\(/', $function); |
||||
self::assertStringNotContainsString('php::call(get_persistent_class(', $function); |
||||
} |
||||
|
||||
private function compileFixture(): string |
||||
{ |
||||
global $translator; |
||||
|
||||
$compiler = CompilerTest::create(ROOT_PATH); |
||||
$translator = $compiler; |
||||
$source = ROOT_PATH . '/phpunit/code/clone-with-codegen.php'; |
||||
$compiler->addFiles([$source]); |
||||
$compiler->prepareFile($source); |
||||
$generated = $compiler->convertFile($source); |
||||
$code = file_get_contents($generated); |
||||
|
||||
self::assertIsString($code); |
||||
return $code; |
||||
} |
||||
|
||||
private function functionBody(string $code, string $function): string |
||||
{ |
||||
$start = strpos($code, $function . '('); |
||||
self::assertIsInt($start); |
||||
$end = strpos($code, "\n}\n", $start); |
||||
self::assertIsInt($end); |
||||
|
||||
return substr($code, $start, $end - $start + 3); |
||||
} |
||||
} |
||||
@ -0,0 +1,98 @@ |
||||
--TEST-- |
||||
PHP 8.5 clone-with supports one/two arguments and preserves evaluation order |
||||
--SKIPIF-- |
||||
<?php |
||||
if (PHP_VERSION_ID < 80500) { |
||||
die('skip requires PHP 8.5'); |
||||
} |
||||
?> |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class CloneWithRecord |
||||
{ |
||||
public function __construct( |
||||
public string $name, |
||||
public int $revision, |
||||
public array $tags, |
||||
) {} |
||||
|
||||
public function __clone(): void |
||||
{ |
||||
echo "__clone:", $this->name, "\n"; |
||||
$this->revision++; |
||||
} |
||||
} |
||||
|
||||
function clone_with_source(CloneWithRecord $source): CloneWithRecord |
||||
{ |
||||
echo "source\n"; |
||||
return $source; |
||||
} |
||||
|
||||
function clone_with_updates(): array |
||||
{ |
||||
echo "updates\n"; |
||||
return ['name' => 'dynamic', 'revision' => 30]; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$source = new CloneWithRecord('original', 1, ['source']); |
||||
|
||||
$plain = clone($source); |
||||
$literal = clone($source, [ |
||||
'name' => 'literal', |
||||
'tags' => ['literal'], |
||||
]); |
||||
$dynamic = \clone(clone_with_source($source), clone_with_updates()); |
||||
$named = clone(object: $source, withProperties: ['name' => 'named']); |
||||
$unpacked = clone(...[ |
||||
'object' => $source, |
||||
'withProperties' => ['name' => 'unpacked'], |
||||
]); |
||||
|
||||
var_dump($source->name, $source->revision, $source->tags); |
||||
var_dump($plain->name, $plain->revision, $plain->tags); |
||||
var_dump($literal->name, $literal->revision, $literal->tags); |
||||
var_dump($dynamic->name, $dynamic->revision, $dynamic->tags); |
||||
var_dump($named->name, $named->revision); |
||||
var_dump($unpacked->name, $unpacked->revision); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
__clone:original |
||||
__clone:original |
||||
source |
||||
updates |
||||
__clone:original |
||||
__clone:original |
||||
__clone:original |
||||
string(8) "original" |
||||
int(1) |
||||
array(1) { |
||||
[0]=> |
||||
string(6) "source" |
||||
} |
||||
string(8) "original" |
||||
int(2) |
||||
array(1) { |
||||
[0]=> |
||||
string(6) "source" |
||||
} |
||||
string(7) "literal" |
||||
int(2) |
||||
array(1) { |
||||
[0]=> |
||||
string(7) "literal" |
||||
} |
||||
string(7) "dynamic" |
||||
int(30) |
||||
array(1) { |
||||
[0]=> |
||||
string(6) "source" |
||||
} |
||||
string(5) "named" |
||||
int(2) |
||||
string(8) "unpacked" |
||||
int(2) |
||||
@ -0,0 +1,36 @@ |
||||
--TEST-- |
||||
PHP 8.5 clone is available as a first-class callable and string callable |
||||
--SKIPIF-- |
||||
<?php |
||||
if (PHP_VERSION_ID < 80500) { |
||||
die('skip requires PHP 8.5'); |
||||
} |
||||
?> |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class CloneCallableValue |
||||
{ |
||||
public function __construct(public int $value) {} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$source = new CloneCallableValue(7); |
||||
$callable = clone(...); |
||||
|
||||
$first = $callable($source, ['value' => 8]); |
||||
$mapped = array_map('clone', [$source, $first]); |
||||
|
||||
var_dump($source !== $first, $first->value); |
||||
var_dump($mapped[0] !== $source, $mapped[0]->value); |
||||
var_dump($mapped[1] !== $first, $mapped[1]->value); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
bool(true) |
||||
int(8) |
||||
bool(true) |
||||
int(7) |
||||
bool(true) |
||||
int(8) |
||||
@ -0,0 +1,36 @@ |
||||
--TEST-- |
||||
PHP 8.5 clone-with supports dynamic, numeric and object-valued properties |
||||
--SKIPIF-- |
||||
<?php |
||||
if (PHP_VERSION_ID < 80500) { |
||||
die('skip requires PHP 8.5'); |
||||
} |
||||
?> |
||||
--FILE-- |
||||
<?php |
||||
|
||||
function main(): void |
||||
{ |
||||
$source = new stdClass(); |
||||
$source->original = 'source'; |
||||
|
||||
$copy = clone($source, [ |
||||
0 => 'zero', |
||||
'named' => 'value', |
||||
'source' => $source, |
||||
]); |
||||
$properties = get_object_vars($copy); |
||||
|
||||
var_dump($source !== $copy); |
||||
var_dump($source->original, $copy->original); |
||||
var_dump($properties[0], $properties['named']); |
||||
var_dump($copy->source === $source); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
bool(true) |
||||
string(6) "source" |
||||
string(6) "source" |
||||
string(4) "zero" |
||||
string(5) "value" |
||||
bool(true) |
||||
@ -0,0 +1,57 @@ |
||||
--TEST-- |
||||
PHP 8.5 clone-with stops property updates at the first error |
||||
--SKIPIF-- |
||||
<?php |
||||
if (PHP_VERSION_ID < 80500) { |
||||
die('skip requires PHP 8.5'); |
||||
} |
||||
?> |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class CloneWithErrors |
||||
{ |
||||
public int $value = 1; |
||||
|
||||
public function __set(string $name, mixed $value): void |
||||
{ |
||||
echo $name, ':', $value, "\n"; |
||||
if ($name === 'stop') { |
||||
throw new RuntimeException('rejected ' . $value); |
||||
} |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$source = new CloneWithErrors(); |
||||
|
||||
try { |
||||
clone($source, [ |
||||
'before' => 'first', |
||||
'stop' => 'reject', |
||||
'after' => 'last', |
||||
]); |
||||
} catch (RuntimeException $error) { |
||||
echo $error->getMessage(), "\n"; |
||||
} |
||||
|
||||
try { |
||||
clone($source, ['value' => 'invalid']); |
||||
} catch (TypeError $error) { |
||||
echo $error::class, ":property\n"; |
||||
} |
||||
|
||||
try { |
||||
clone($source, 42); |
||||
} catch (TypeError $error) { |
||||
echo $error::class, ":argument\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
before:first |
||||
stop:reject |
||||
rejected reject |
||||
TypeError:property |
||||
TypeError:argument |
||||
@ -0,0 +1,31 @@ |
||||
--TEST-- |
||||
PHP 8.5 clone-with rejects active references and unwraps a sole remaining reference |
||||
--SKIPIF-- |
||||
<?php |
||||
if (PHP_VERSION_ID < 80500) { |
||||
die('skip requires PHP 8.5'); |
||||
} |
||||
?> |
||||
--FILE-- |
||||
<?php |
||||
|
||||
function main(): void |
||||
{ |
||||
$source = new stdClass(); |
||||
$value = 'reference'; |
||||
$updates = ['value' => &$value]; |
||||
|
||||
try { |
||||
clone($source, $updates); |
||||
} catch (Error $error) { |
||||
echo $error->getMessage(), "\n"; |
||||
} |
||||
|
||||
unset($value); |
||||
$copy = clone($source, $updates); |
||||
var_dump($copy->value); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
Cannot assign by reference when cloning with updated properties |
||||
string(9) "reference" |
||||
@ -0,0 +1,96 @@ |
||||
--TEST-- |
||||
PHP 8.5 clone-with respects property scope and unlocks readonly properties |
||||
--SKIPIF-- |
||||
<?php |
||||
if (PHP_VERSION_ID < 80500) { |
||||
die('skip requires PHP 8.5'); |
||||
} |
||||
?> |
||||
--XFAIL-- |
||||
TypePHP internal classes do not yet preserve private/protected/readonly property scope during clone-with |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class CloneWithScopeBase |
||||
{ |
||||
private int $privateValue = 1; |
||||
protected int $protectedValue = 2; |
||||
public readonly int $readonlyValue; |
||||
|
||||
public function __construct() |
||||
{ |
||||
$this->readonlyValue = 3; |
||||
} |
||||
|
||||
public function withPrivateAndReadonly(): self |
||||
{ |
||||
return clone($this, [ |
||||
'privateValue' => 10, |
||||
'readonlyValue' => 30, |
||||
]); |
||||
} |
||||
|
||||
public function values(): array |
||||
{ |
||||
return [$this->privateValue, $this->protectedValue, $this->readonlyValue]; |
||||
} |
||||
} |
||||
|
||||
class CloneWithScopeChild extends CloneWithScopeBase |
||||
{ |
||||
public function withProtected(): self |
||||
{ |
||||
return clone($this, ['protectedValue' => 20]); |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$source = new CloneWithScopeChild(); |
||||
$privateCopy = $source->withPrivateAndReadonly(); |
||||
$protectedCopy = $source->withProtected(); |
||||
|
||||
var_dump($source->values()); |
||||
var_dump($privateCopy->values()); |
||||
var_dump($protectedCopy->values()); |
||||
|
||||
try { |
||||
clone($source, ['protectedValue' => 99]); |
||||
} catch (Error $error) { |
||||
echo $error->getMessage(), "\n"; |
||||
} |
||||
|
||||
try { |
||||
clone($source, ['readonlyValue' => 99]); |
||||
} catch (Error $error) { |
||||
echo $error->getMessage(), "\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(3) { |
||||
[0]=> |
||||
int(1) |
||||
[1]=> |
||||
int(2) |
||||
[2]=> |
||||
int(3) |
||||
} |
||||
array(3) { |
||||
[0]=> |
||||
int(10) |
||||
[1]=> |
||||
int(2) |
||||
[2]=> |
||||
int(30) |
||||
} |
||||
array(3) { |
||||
[0]=> |
||||
int(1) |
||||
[1]=> |
||||
int(20) |
||||
[2]=> |
||||
int(3) |
||||
} |
||||
Cannot access protected property CloneWithScopeChild::$protectedValue |
||||
Cannot modify protected(set) readonly property CloneWithScopeBase::$readonlyValue from global scope |
||||
Loading…
Reference in new issue