- 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