重构 readonly:__construct() 和 __clone() 都属于初始化窗口 ,其他地方不允许写。与 PHP 不一致。

pull/48/head
韩天峰 2 weeks ago
parent a3ea91918d
commit e5f61eabfe
  1. 1
      docs/INCOMPATIBLE_PHP_FEATURES.md
  2. 1
      docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md
  3. 13
      phpunit/code/clone-direct-method-call.php
  4. 13
      phpunit/code/clone-direct-static-call.php
  5. 10
      phpunit/code/constructor-direct-method-call.php
  6. 10
      phpunit/code/constructor-direct-static-call.php
  7. 17
      phpunit/code/readonly-property-no-native-ref.php
  8. 10
      phpunit/code/readonly-reference-assignment.php
  9. 16
      phpunit/code/readonly-reference-call-argument.php
  10. 11
      phpunit/code/readonly-reference-fetch.php
  11. 7
      phpunit/code/readonly-write-array-dim.php
  12. 19
      phpunit/code/readonly-write-child-clone.php
  13. 13
      phpunit/code/readonly-write-child-constructor.php
  14. 19
      phpunit/code/readonly-write-clone-closure.php
  15. 7
      phpunit/code/readonly-write-coalesce.php
  16. 7
      phpunit/code/readonly-write-compound.php
  17. 13
      phpunit/code/readonly-write-constructor-closure.php
  18. 10
      phpunit/code/readonly-write-foreach.php
  19. 7
      phpunit/code/readonly-write-increment.php
  20. 7
      phpunit/code/readonly-write-list.php
  21. 17
      phpunit/code/readonly-write-other-instance-clone.php
  22. 10
      phpunit/code/readonly-write-other-instance-constructor.php
  23. 15
      phpunit/code/readonly-write-outside-constructor.php
  24. 15
      phpunit/src/NativePropertyTest.php
  25. 77
      phpunit/src/ReadonlyPropertyTest.php
  26. 5
      src/CompilerBase.php
  27. 3
      src/Entity/MethodDef.php
  28. 6
      src/Generator/CallArgumentGenerator.php
  29. 5
      src/Parser/ArrayExpressionTrait.php
  30. 12
      src/Parser/AssignOpTrait.php
  31. 4
      src/Parser/ForeachTrait.php
  32. 53
      src/Parser/MethodCallTrait.php
  33. 80
      src/Parser/PropertyAccessTrait.php
  34. 1
      src/Translator.php
  35. 3
      src/TypeSystem/NativeTypeCompatibilityTrait.php
  36. 68
      tests/compiler/object_property/readonly-clone-initialization.phpt
  37. 42
      tests/compiler/object_property/readonly-constructor-only.phpt
  38. 35
      tests/compiler/object_property/readonly-dynamic-clone-call.phpt
  39. 30
      tests/compiler/object_property/readonly-dynamic-constructor-call.phpt

@ -48,6 +48,7 @@
- 禁止子类用同名 `private` 属性隐藏父类私有属性;`public` / `protected` 同名声明视为同一个继承 property slot,仍须满足类型、可见性和 `readonly` 兼容性要求。
- 为避免 typed property 写入路径引入额外动态检查,native typed property 在右值类型不确定或与属性类型不一致时会退化为 `setProperty()`;部分标量赋值可能遵循 Zend 弱类型转换,而不是 AOT 默认 strict 语义。
- constructor property promotion 的运行时属性可用,但 `ReflectionProperty::isPromoted()` 目前不返回标准 PHP 结果。
- `readonly` 使用“初始化窗口”语义,而不是 PHP 的“一次赋值”语义:属性只能在声明类自己的 `__construct()``__clone()` 中通过直接 `$this` 写入,且窗口内允许重复修改;其他方法、子类初始化方法、嵌套闭包、其他对象、引用和 `unset()` 均不可写。`__construct()` / `__clone()` 不能作为普通方法调用,合法的 `parent::__construct()` / `parent::__clone()` 初始化链除外。
## 表达式与控制流

@ -76,6 +76,7 @@ These items should be documented with the exact boundary.
| Reserved keyword methods such as `toArray()` | Intentional Rule | Conversion keywords are resolved before ordinary object methods to keep conversion lowering static and predictable. |
| Zero-initialized fixed typed property slots | Intentional Rule / Partial | Native fixed-layout slots use their type's zero value instead of preserving every Zend uninitialized-property transition. |
| Structural mutation of `std` containers during `foreach` | Intentional Rule | Native C++ iterators may be invalidated by append, insertion, erase or whole-container replacement. TypePHP rejects these operations inside the active loop while allowing non-structural element updates. |
| `readonly` initialization window | Intentional Rule | A readonly property is writable only through direct `$this` access in its declaring class' `__construct()` or `__clone()`. Repeated initialization writes are allowed there; after either initialization path it is frozen. Ordinary calls to these lifecycle methods are rejected, while lexical parent initialization chains remain valid. |
## Implementable but Currently Unsupported

@ -0,0 +1,13 @@
<?php
class CloneDirectMethodCall
{
public function __clone(): void
{
}
public function invoke(): void
{
$this->__clone();
}
}

@ -0,0 +1,13 @@
<?php
class CloneDirectStaticCall
{
public function __clone(): void
{
}
public function invoke(): void
{
self::__clone();
}
}

@ -0,0 +1,10 @@
<?php
class ConstructorDirectMethodCall
{
public function __construct() {}
}
function invoke_constructor(ConstructorDirectMethodCall $object): void
{
$object->__construct();
}

@ -0,0 +1,10 @@
<?php
class ConstructorDirectStaticCall
{
public function __construct() {}
public function invoke(): void
{
self::__construct();
}
}

@ -0,0 +1,17 @@
<?php
use native_types;
class ReadonlyPropertyNoNativeRef
{
public readonly int $integer;
public readonly float $floating;
public function __construct(int $integer, float $floating)
{
$this->integer = $integer;
$this->floating = $floating;
$this->integer += 1;
$this->floating += 1.5;
}
}

@ -0,0 +1,10 @@
<?php
class ReadonlyReferenceAssignment
{
public readonly int $value;
public function __construct(int &$source)
{
$this->value =& $source;
}
}

@ -0,0 +1,16 @@
<?php
function mutate_readonly_argument(int &$value): void
{
$value++;
}
class ReadonlyReferenceCallArgument
{
public readonly int $value;
public function __construct()
{
$this->value = 1;
mutate_readonly_argument($this->value);
}
}

@ -0,0 +1,11 @@
<?php
class ReadonlyReferenceFetch
{
public readonly int $value;
public function __construct()
{
$this->value = 1;
$reference =& $this->value;
}
}

@ -0,0 +1,7 @@
<?php
class ReadonlyArrayDimWrite
{
public readonly array $value;
public function __construct() { $this->value = []; }
public function change(): void { $this->value[] = 1; }
}

@ -0,0 +1,19 @@
<?php
class ReadonlyCloneParent
{
public readonly int $value;
public function __construct()
{
$this->value = 1;
}
}
class ReadonlyCloneChild extends ReadonlyCloneParent
{
public function __clone(): void
{
$this->value = 2;
}
}

@ -0,0 +1,13 @@
<?php
class ReadonlyParent
{
protected readonly int $value;
}
class ReadonlyChild extends ReadonlyParent
{
public function __construct()
{
$this->value = 1;
}
}

@ -0,0 +1,19 @@
<?php
class ReadonlyCloneClosure
{
public readonly int $value;
public function __construct()
{
$this->value = 1;
}
public function __clone(): void
{
$write = function (): void {
$this->value = 2;
};
$write();
}
}

@ -0,0 +1,7 @@
<?php
class ReadonlyCoalesceWrite
{
public readonly ?int $value;
public function __construct() { $this->value = null; }
public function change(): void { $this->value ??= 1; }
}

@ -0,0 +1,7 @@
<?php
class ReadonlyCompoundWrite
{
public readonly int $value;
public function __construct() { $this->value = 1; }
public function change(): void { $this->value += 1; }
}

@ -0,0 +1,13 @@
<?php
class ReadonlyConstructorClosure
{
public readonly int $value;
public function __construct()
{
$writer = function (): void {
$this->value = 1;
};
$writer();
}
}

@ -0,0 +1,10 @@
<?php
class ReadonlyForeachWrite
{
public readonly int $value;
public function __construct() { $this->value = 1; }
public function change(): void
{
foreach ([2] as $this->value) {}
}
}

@ -0,0 +1,7 @@
<?php
class ReadonlyIncrementWrite
{
public readonly int $value;
public function __construct() { $this->value = 1; }
public function change(): void { ++$this->value; }
}

@ -0,0 +1,7 @@
<?php
class ReadonlyListWrite
{
public readonly int $value;
public function __construct() { $this->value = 1; }
public function change(): void { [$this->value] = [2]; }
}

@ -0,0 +1,17 @@
<?php
class ReadonlyCloneOtherInstance
{
public readonly int $value;
public function __construct()
{
$this->value = 1;
}
public function __clone(): void
{
$other = new self();
$other->value = 2;
}
}

@ -0,0 +1,10 @@
<?php
class ReadonlyOtherInstance
{
public readonly int $value;
public function __construct(ReadonlyOtherInstance $other)
{
$other->value = 1;
}
}

@ -0,0 +1,15 @@
<?php
class ReadonlyWriteOutsideConstructor
{
public readonly int $value;
public function __construct()
{
$this->value = 1;
}
public function change(): void
{
$this->value = 2;
}
}

@ -60,6 +60,21 @@ class NativePropertyTest extends \BaseTest
$this->assertStringNotContainsString('box.attr(php_get_prop(0, _literal_strings[0], 0, _literal_strings[1]), true) +=', $code);
}
public function testReadonlyPropertiesDoNotUseNativeScalarReferences(): void
{
try {
$outputFile = $this->compileNativeProperty('readonly-property-no-native-ref.php');
} catch (TestError $e) {
$this->fail($e->getMessage());
}
$code = file_get_contents($outputFile);
$this->assertStringNotContainsString('typephp_static_int_ref(', $code);
$this->assertStringNotContainsString('typephp_static_float_ref(', $code);
$this->assertStringNotContainsString('_object_prop_', $code);
$this->assertStringContainsString('.attr(', $code);
}
public function testNativeIntPropertyAssignOpConvertsBitwiseNotClassConst(): void
{
try {

@ -0,0 +1,77 @@
<?php
class ReadonlyPropertyTest extends \BaseTest
{
public function testConstructorCannotBeCalledAsOrdinaryMethod(): void
{
$this->exec('Constructor __construct() can only be invoked by new', 'constructor-direct-method-call.php');
$this->exec('Constructor __construct() can only be invoked by new', 'constructor-direct-static-call.php');
}
public function testCloneCannotBeCalledAsOrdinaryMethod(): void
{
$this->exec('Clone method __clone() can only be invoked by clone', 'clone-direct-method-call.php');
$this->exec('Clone method __clone() can only be invoked by clone', 'clone-direct-static-call.php');
}
public function testWriteOutsideConstructorIsRejected(): void
{
$this->exec('Readonly property `ReadonlyWriteOutsideConstructor::$value` can only be modified in its declaring `__construct` or `__clone` method', 'readonly-write-outside-constructor.php');
}
public function testChildConstructorCannotWriteParentReadonlyProperty(): void
{
$this->exec('Readonly property `ReadonlyParent::$value` can only be modified in its declaring `__construct` or `__clone` method', 'readonly-write-child-constructor.php');
}
public function testConstructorCannotWriteReadonlyPropertyOnAnotherObject(): void
{
$this->exec('Readonly property `ReadonlyOtherInstance::$value` can only be modified on `$this`', 'readonly-write-other-instance-constructor.php');
}
public function testClosureInsideConstructorCannotWriteReadonlyProperty(): void
{
$this->exec('Readonly property `ReadonlyConstructorClosure::$value` can only be modified directly in `__construct` or `__clone`', 'readonly-write-constructor-closure.php');
}
public function testReadonlyCloneWriteRetainsLexicalRestrictions(): void
{
$this->exec(
'Readonly property `ReadonlyCloneParent::$value` can only be modified in its declaring `__construct` or `__clone` method',
'readonly-write-child-clone.php'
);
$this->exec(
'Readonly property `ReadonlyCloneClosure::$value` can only be modified directly in `__construct` or `__clone`',
'readonly-write-clone-closure.php'
);
$this->exec(
'Readonly property `ReadonlyCloneOtherInstance::$value` can only be modified on `$this`',
'readonly-write-other-instance-clone.php'
);
}
public function testReadonlyPropertyCannotBeAssignedByReference(): void
{
$this->exec('Cannot assign readonly property `ReadonlyReferenceAssignment::$value` by reference', 'readonly-reference-assignment.php');
}
public function testReadonlyPropertyCannotBeTakenByReference(): void
{
$this->exec('Cannot take reference to readonly property `ReadonlyReferenceFetch::$value`', 'readonly-reference-fetch.php');
$this->exec('Cannot take reference to readonly property `ReadonlyReferenceCallArgument::$value`', 'readonly-reference-call-argument.php');
}
public function testAllReadonlyWriteFormsOutsideConstructorAreRejected(): void
{
foreach ([
'readonly-write-compound.php',
'readonly-write-increment.php',
'readonly-write-array-dim.php',
'readonly-write-coalesce.php',
'readonly-write-list.php',
'readonly-write-foreach.php',
] as $file) {
$this->exec('can only be modified in its declaring `__construct` or `__clone` method', $file);
}
}
}

@ -1095,6 +1095,11 @@ class CompilerBase implements PropertyAccessContext
return $this->method === '__construct';
}
protected function isCurrentCloneMethod(): bool
{
return $this->method === '__clone';
}
protected function getCurrentMethodDisplayName(): string
{
return $this->getFullClassName() . '::' . $this->method;

@ -25,6 +25,9 @@ class MethodDef
/** Source trait, retained only for diagnostics and the __TRAIT__ constant. */
public string $traitOrigin = '';
/** Original trait method name before a use-site alias is applied. */
public string $traitMethod = '';
public function __construct(int $flags, string $name)
{
$this->flags = $flags;

@ -418,6 +418,9 @@ trait CallArgumentGenerator
$namedArgs[$arg->name->name] = true;
$byRef = ($funcName && $this->isReferenceNamedArgument($funcName, $className, $arg->name->name))
|| ($preserveExistingReferences && $this->isExistingReferenceCallArg($arg));
if ($byRef) {
$this->assertReadonlyPropertyReferenceForbidden($arg->value, $arg, false);
}
$value = ($byRef || $this->isRefvalCall($arg->value) || $this->isToRefCall($arg->value))
? $this->parseReferenceCallArgValue($arg)
: $this->parseCallArgValue($arg);
@ -438,6 +441,9 @@ trait CallArgumentGenerator
}
$byRef = ($funcName && $this->isReferenceArgument($funcName, $className, $i))
|| ($preserveExistingReferences && $this->isExistingReferenceCallArg($arg));
if ($byRef) {
$this->assertReadonlyPropertyReferenceForbidden($arg->value, $arg, false);
}
if (($funcName === 'call_user_func' || $funcName === 'call_user_func_array') && $i === 0) {
$callback = $this->parseScopedCallbackArg($arg);
if ($callback !== null) {

@ -106,6 +106,11 @@ trait ArrayExpressionTrait
}
if ($expr instanceof Expr\PropertyFetch) {
// Keep the write-policy check at the common writable-expression
// boundary as well as at assignment lowering sites. This covers
// destructuring, foreach targets, and future write forms that use
// parseWritableIdentifier() directly.
$this->preparePropertyWriteTarget($expr);
return $this->parsePropertyFetchUpdate($expr);
}

@ -624,6 +624,12 @@ trait AssignOpTrait
if ($def === null) {
return null;
}
if ($def->isReadonly()) {
// A native scalar reference mutates the property zval directly and
// bypasses Zend's readonly checks. Keep readonly properties on the
// normal attr() path so no raw scalar reference escapes the wrapper.
return null;
}
$rightType = $this->detectTypeOfExpr($node->expr);
if ($this->isFixedObjectProp($def) && $rightType !== Type::VAR && !$this->canAssignStaticTypeToObjectProperty($def, $rightType)) {
@ -782,6 +788,12 @@ trait AssignOpTrait
$this->fatalError($expr->expr, 'Cannot take reference of a nullsafe chain');
}
// A reference would outlive the constructor-only write window and
// make later mutations invisible to the compiler. It is therefore
// forbidden on either side even inside the declaring constructor.
$this->assertReadonlyPropertyReferenceForbidden($expr->var, $expr, true);
$this->assertReadonlyPropertyReferenceForbidden($expr->expr, $expr, false);
$left = $this->parseWritableIdentifier($expr->var);
// Keep this write-context form for every RHS kind. Re-parsing it as a
// read later breaks append and missing-key targets such as

@ -91,7 +91,9 @@ trait ForeachTrait
return $this->getIndent() . "{$array}.offsetSet({$dim}, {$valueExpr});";
}
$valueVar = $this->parseIdentifier($node->valueVar);
$valueVar = $this->isPropertyFetch($node->valueVar)
? $this->parseWritableIdentifier($node->valueVar)
: $this->parseIdentifier($node->valueVar);
if ($node->byRef) {
if (!$this->hasVar($valueVar)) {
$this->addLocalVar($valueVar, Type::REF);

@ -276,6 +276,25 @@ trait MethodCallTrait
}
return $this->genRuntimeFunctionCall($callable, $expr->args, $method, $parentClass);
}
if ($method === '__construct') {
if (!$this->isConstructorImplementationContext() || $this->context->inClosure) {
$this->fatalError($expr, 'Constructor __construct() can only be invoked by new');
}
if (empty($expr->args)) {
return 'typephp_call_parent_constructor(this_, ' . $methodPtr . ')';
}
return 'typephp_call_parent_constructor(this_, ' . $methodPtr . ', '
. $this->parseCallArgs($expr->args, $method, $parentClass) . ')';
}
if ($method === '__clone') {
if (!$this->isCurrentCloneMethod() || $this->context->inClosure) {
$this->fatalError($expr, 'Clone method __clone() can only be invoked by clone');
}
if (!empty($expr->args)) {
$this->fatalError($expr, 'Clone method __clone() does not accept arguments');
}
return 'typephp_call_parent_clone(this_, ' . $methodPtr . ')';
}
if (empty($expr->args)) {
return 'this_.call(' . $methodPtr . ')';
}
@ -303,6 +322,15 @@ trait MethodCallTrait
protected function parseMethodCall(Expr\MethodCall $expr): string
{
if ($this->isNamedMethod($expr->name)) {
$methodName = strtolower($expr->name->toString());
if ($methodName === '__construct') {
$this->fatalError($expr, 'Constructor __construct() can only be invoked by new');
}
if ($methodName === '__clone') {
$this->fatalError($expr, 'Clone method __clone() can only be invoked by clone');
}
}
if ($this->containsNullsafeChain($expr->var)) {
return $this->parseNullsafeExpr($expr);
}
@ -510,6 +538,12 @@ trait MethodCallTrait
}
}
private function isConstructorImplementationContext(): bool
{
return $this->isCurrentConstructor()
|| strtolower($this->methodDef?->traitMethod ?? '') === '__construct';
}
private function isDefinitelyObjectReceiver(
Expr $receiver,
string $object,
@ -573,6 +607,25 @@ trait MethodCallTrait
$rtClass = '';
$class = $this->parseIdentifier($expr->class);
if ($this->isIdExpr($expr->name) && strtolower($expr->name->toString()) === '__construct') {
$isParentConstructor = $this->isNameExpr($expr->class)
&& $class === 'parent'
&& $this->isConstructorImplementationContext()
&& !$this->context->inClosure;
if (!$isParentConstructor) {
$this->fatalError($expr, 'Constructor __construct() can only be invoked by new');
}
}
if ($this->isIdExpr($expr->name) && strtolower($expr->name->toString()) === '__clone') {
$isParentClone = $this->isNameExpr($expr->class)
&& $class === 'parent'
&& $this->isCurrentCloneMethod()
&& !$this->context->inClosure;
if (!$isParentClone) {
$this->fatalError($expr, 'Clone method __clone() can only be invoked by clone');
}
}
// parent::$method() still has a lexical parent class even when the
// method name itself is dynamic. Handle it before the generic dynamic
// static-call branch below.

@ -147,6 +147,9 @@ trait PropertyAccessTrait
protected function emitDynamicPropertyFetchAppendArray(Expr\PropertyFetch $expr, string $value, ?PropertyWriteTarget $target = null): string
{
if ($this->isNativePropertyAccess($expr)) {
return $this->parseWritableIdentifier($expr) . ".newItem() = {$value}";
}
if ($this->canEmitDynamicPropertyTarget($target)) {
return $this->emitDynamicPropertyTargetAppendArray($target, $value);
}
@ -160,6 +163,9 @@ trait PropertyAccessTrait
protected function emitDynamicPropertyFetchUpdateArray(Expr\PropertyFetch $expr, string $dim, string $value, ?PropertyWriteTarget $target = null): string
{
if ($this->isNativePropertyAccess($expr)) {
return $this->parseWritableIdentifier($expr) . ".item({$dim}, true) = {$value}";
}
if ($this->canEmitDynamicPropertyTarget($target)) {
return $this->emitDynamicPropertyTargetUpdateArray($target, $dim, $value);
}
@ -437,7 +443,7 @@ trait PropertyAccessTrait
$this->assertCanAssignObjectProperty($left, $right, 'static property');
}
protected function preparePropertyWriteTarget(NodeAbstract $left): ?PropertyWriteTarget
protected function preparePropertyWriteTarget(NodeAbstract $left, bool $checkReadonlyWrite = true): ?PropertyWriteTarget
{
if ($left instanceof Expr\PropertyFetch) {
$objectExpr = null;
@ -449,6 +455,9 @@ trait PropertyAccessTrait
if ($this->isIdExpr($left->name)) {
$this->getPropertyIdentifier($left, $left->var, $left->name);
$this->assertPropertySetVisibility($left);
if ($checkReadonlyWrite) {
$this->assertReadonlyPropertyWriteContext($left);
}
}
return new PropertyWriteTarget($left, 'object property', $objectExpr, $propertyExpr);
}
@ -464,6 +473,71 @@ trait PropertyAccessTrait
return null;
}
/**
* TypePHP intentionally gives readonly an initialization-phase meaning:
* only the declaring class' __construct or __clone body may write its own
* property. This check is lexical; a nested closure is a different
* function and must not inherit the write privilege.
*/
private function assertReadonlyPropertyWriteContext(Expr\PropertyFetch $property): void
{
$access = $this->getNativePropertyAccess($property);
if ($access === null || !$access->getPropertyDef()->isReadonly()) {
return;
}
$declaringClass = $access->resolution->declaringClass;
$propertyName = $this->parseIdentifier($property->name);
$display = $declaringClass . '::$' . $propertyName;
if ($this->context->inClosure) {
$this->fatalError(
$property,
"Readonly property `{$display}` can only be modified directly in `__construct` or `__clone`"
);
}
if ((!$this->isCurrentConstructor() && !$this->isCurrentCloneMethod())
|| !$this->isSameClassName($this->getFullClassName(), $declaringClass)) {
$this->fatalError(
$property,
"Readonly property `{$display}` can only be modified in its declaring `__construct` or `__clone` method"
);
}
if (!$this->isVarExpr($property->var) || $this->parseIdentifier($property->var) !== 'this_') {
$this->fatalError(
$property,
"Readonly property `{$display}` can only be modified on `\$this`"
);
}
}
protected function assertReadonlyPropertyReferenceForbidden(
NodeAbstract $expr,
NodeAbstract $errorNode,
bool $assignmentTarget,
): void {
while ($expr instanceof Expr\ArrayDimFetch) {
$expr = $expr->var;
}
if (!$expr instanceof Expr\PropertyFetch || !$this->isIdExpr($expr->name)) {
return;
}
$this->getPropertyIdentifier($expr, $expr->var, $expr->name);
$access = $this->getNativePropertyAccess($expr);
if ($access === null || !$access->getPropertyDef()->isReadonly()) {
return;
}
$display = $access->resolution->declaringClass . '::$' . $this->parseIdentifier($expr->name);
$message = $assignmentTarget
? "Cannot assign readonly property `{$display}` by reference"
: "Cannot take reference to readonly property `{$display}`";
$this->fatalError($errorNode, $message);
}
private function assertPropertySetVisibility(NodeAbstract $property): void
{
if ($this->isPropertyHookBackingAccess($property)) {
@ -730,7 +804,9 @@ trait PropertyAccessTrait
$lines[] = $array . '.offsetUnset(' . $dim . ');';
}
} elseif ($this->isPropertyFetch($var)) {
$propertyWriteTarget = $this->preparePropertyWriteTarget($var);
// unset has its own unconditional readonly diagnostic below;
// it is forbidden even while __construct is running.
$propertyWriteTarget = $this->preparePropertyWriteTarget($var, false);
$object = $this->getDynamicPropertyFetchObjectExpr($var, $propertyWriteTarget);
$restoreDefault = null;
if ($this->isIdExpr($var->name)) {

@ -4630,6 +4630,7 @@ CODE;
$methodDef = new MethodDef($flags, $name);
$methodDef->node = $methodStmt;
$methodDef->traitOrigin = (string) $methodStmt->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE, '');
$methodDef->traitMethod = (string) $methodStmt->getAttribute(self::TRAIT_METHOD_ATTRIBUTE, '');
$this->method = $name;
$this->methodDef = $methodDef;

@ -162,6 +162,7 @@ trait NativeTypeCompatibilityTrait
if ($argInfo->byRef) {
if ($this->isReferenceWrapperCall($arg->value)) {
$inner = $this->unwrapReferenceWrapperCall($arg->value, $arg);
$this->assertReadonlyPropertyReferenceForbidden($inner, $arg, false);
if ($this->isVarExpr($inner)) {
$arg->value = $inner;
} else {
@ -171,6 +172,8 @@ trait NativeTypeCompatibilityTrait
}
$this->fatalError($arg, 'The refval function only accepts a variable, array element, or object property');
}
} else {
$this->assertReadonlyPropertyReferenceForbidden($arg->value, $arg, false);
}
if ($this->isVarExpr($arg->value)) {
$var = $this->parseVariable($arg->value);

@ -0,0 +1,68 @@
--TEST--
TypePHP readonly properties may be updated while cloning
--FILE--
<?php
use native_types;
class ReadonlyCloneBase
{
public readonly int $base;
public function __construct()
{
$this->base = 1;
}
public function __clone(): void
{
$this->base++;
$this->base += 3;
}
}
class ReadonlyCloneValue extends ReadonlyCloneBase
{
public readonly string $name;
public readonly array $items;
public function __construct()
{
parent::__construct();
$this->name = 'original';
$this->items = [1];
}
public function __clone(): void
{
parent::__clone();
$this->name = 'clone';
$this->name .= 'd';
$this->items[] = 2;
$this->items[0] = 10;
}
}
function main(): void
{
$original = new ReadonlyCloneValue();
$copy = clone $original;
var_dump($original->base, $original->name, $original->items);
var_dump($copy->base, $copy->name, $copy->items);
}
?>
--EXPECT--
int(1)
string(8) "original"
array(1) {
[0]=>
int(1)
}
int(5)
string(6) "cloned"
array(2) {
[0]=>
int(10)
[1]=>
int(2)
}

@ -0,0 +1,42 @@
--TEST--
TypePHP readonly properties are mutable only during their declaring constructor
--FILE--
<?php
use native_types;
class ReadonlyConstructorOnly
{
public readonly int $number;
public readonly string $text;
public readonly array $items;
public function __construct()
{
$this->number = 1;
$this->number = 2;
$this->number += 3;
++$this->number;
$this->text = 'a';
$this->text .= 'b';
$this->items = [];
$this->items[] = 10;
$this->items[0] = 20;
}
}
function main(): void
{
$value = new ReadonlyConstructorOnly();
var_dump($value->number, $value->text, $value->items);
}
?>
--EXPECT--
int(6)
string(2) "ab"
array(1) {
[0]=>
int(20)
}

@ -0,0 +1,35 @@
--TEST--
Readonly clone method cannot be called dynamically
--FILE--
<?php
class ReadonlyDynamicCloneCall
{
public readonly int $value;
public function __construct()
{
$this->value = 1;
}
public function __clone(): void
{
$this->value = 2;
}
}
function main(): void
{
$value = new ReadonlyDynamicCloneCall();
$method = '__clone';
try {
$value->$method();
} catch (Error $error) {
echo $error->getMessage(), "\n";
}
var_dump($value->value);
}
?>
--EXPECT--
Clone method ReadonlyDynamicCloneCall::__clone() can only be invoked by clone
int(1)

@ -0,0 +1,30 @@
--TEST--
Readonly constructor cannot be called dynamically after construction
--FILE--
<?php
class ReadonlyDynamicConstructorCall
{
public readonly int $value;
public function __construct(int $value)
{
$this->value = $value;
}
}
function main(): void
{
$value = new ReadonlyDynamicConstructorCall(1);
$method = '__construct';
try {
$value->$method(2);
} catch (Error $error) {
echo $error->getMessage(), "\n";
}
var_dump($value->value);
}
?>
--EXPECT--
Constructor ReadonlyDynamicConstructorCall::__construct() can only be invoked by new
int(1)
Loading…
Cancel
Save