feat(readonly): enforce direct assignment only for readonly properties

- Remove initialization window semantics for readonly properties
- Restrict readonly property writes to direct assignment only
- Add explicit error messages for unsupported readonly operations
- Update tests to reflect new readonly property behavior
- Remove constructor and clone method invocation restrictions
- Add support for closure rebinding rejection at compile time
- Modify property write target preparation with readonly checks
- Update documentation to remove outdated readonly initialization rules
- Add new test cases for readonly property direct assignment enforcement
pull/48/head
韩天峰 2 weeks ago
parent e5f61eabfe
commit 8118fbd3dc
  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. 8
      phpunit/code/closure/closure-bind-to-unsupported.php
  6. 8
      phpunit/code/closure/closure-bind-unsupported.php
  7. 6
      phpunit/code/closure/closure-call-typed-unsupported.php
  8. 7
      phpunit/code/closure/closure-call-unsupported.php
  9. 10
      phpunit/code/constructor-direct-method-call.php
  10. 10
      phpunit/code/constructor-direct-static-call.php
  11. 2
      phpunit/code/readonly-property-no-native-ref.php
  12. 7
      phpunit/code/readonly-write-array-index.php
  13. 7
      phpunit/code/readonly-write-concat.php
  14. 7
      phpunit/code/readonly-write-decrement.php
  15. 7
      phpunit/code/readonly-write-subtract.php
  16. 22
      phpunit/src/ClosureTest.php
  17. 3
      phpunit/src/NativePropertyTest.php
  18. 57
      phpunit/src/ReadonlyPropertyTest.php
  19. 8
      src/CompilerBase.php
  20. 3
      src/Entity/MethodDef.php
  21. 8
      src/Parser/AssignOpTrait.php
  22. 70
      src/Parser/MethodCallTrait.php
  23. 42
      src/Parser/PropertyAccessTrait.php
  24. 1
      src/Translator.php
  25. 17
      tests/compiler/object_property/readonly-clone-initialization.phpt
  26. 42
      tests/compiler/object_property/readonly-constructor-only.phpt
  27. 35
      tests/compiler/object_property/readonly-dynamic-clone-call.phpt
  28. 30
      tests/compiler/object_property/readonly-dynamic-constructor-call.phpt
  29. 82
      tests/compiler/object_property/readonly-initialization.phpt

@ -48,7 +48,6 @@
- 禁止子类用同名 `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,7 +76,6 @@ 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

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

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

@ -0,0 +1,8 @@
<?php
function closureBindToUnsupported(object $target): void
{
$callback = static function (): void {
};
$callback->bindTo($target);
}

@ -0,0 +1,8 @@
<?php
function closureBindUnsupported(): void
{
$callback = static function (): void {
};
Closure::bind($callback, null);
}

@ -0,0 +1,6 @@
<?php
function closureCallTypedUnsupported(Closure $callback, object $target): void
{
$callback->call($target);
}

@ -0,0 +1,7 @@
<?php
function closureCallUnsupported(): void
{
$callback = static fn(): string => 'value';
echo $callback->call(new stdClass());
}

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

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

@ -11,7 +11,5 @@ class ReadonlyPropertyNoNativeRef
{
$this->integer = $integer;
$this->floating = $floating;
$this->integer += 1;
$this->floating += 1.5;
}
}

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

@ -0,0 +1,7 @@
<?php
class ReadonlyConcatWrite
{
public readonly string $value;
public function __construct() { $this->value = 'a'; }
public function change(): void { $this->value .= 'b'; }
}

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

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

@ -2,7 +2,7 @@
use TypePhp\CompilerTest;
class ClosureTest extends \PHPUnit\Framework\TestCase
class ClosureTest extends \BaseTest
{
public function testUseReferenceCaptureCompiles(): void
{
@ -17,4 +17,24 @@ class ClosureTest extends \PHPUnit\Framework\TestCase
$this->assertTrue(true);
}
public function testClosureRebindingIsRejectedAtCompileTime(): void
{
$this->exec(
'Closure::call() is not supported',
'closure/closure-call-unsupported.php'
);
$this->exec(
'Closure::call() is not supported',
'closure/closure-call-typed-unsupported.php'
);
$this->exec(
'Closure::bindTo() is not supported',
'closure/closure-bind-to-unsupported.php'
);
$this->exec(
'Closure::bind() is not supported',
'closure/closure-bind-unsupported.php'
);
}
}

@ -72,7 +72,8 @@ class NativePropertyTest extends \BaseTest
$this->assertStringNotContainsString('typephp_static_int_ref(', $code);
$this->assertStringNotContainsString('typephp_static_float_ref(', $code);
$this->assertStringNotContainsString('_object_prop_', $code);
$this->assertStringContainsString('.attr(', $code);
$this->assertStringNotContainsString('AttrMode::Update', $code);
$this->assertSame(2, substr_count($code, 'typephp_write_property_scoped('));
}
public function testNativeIntPropertyAssignOpConvertsBitwiseNotClassConst(): void

@ -2,52 +2,19 @@
class ReadonlyPropertyTest extends \BaseTest
{
public function testConstructorCannotBeCalledAsOrdinaryMethod(): void
public function testDirectInitializationIsAllowedOutsideConstructor(): 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');
$this->compile('readonly-write-outside-constructor.php');
$this->compile('readonly-write-child-constructor.php');
$this->compile('readonly-write-constructor-closure.php');
$this->compile('readonly-write-other-instance-constructor.php');
}
public function testCloneCannotBeCalledAsOrdinaryMethod(): void
public function testDirectCloneInitializationUsesTheSameWritePath(): 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'
);
$this->compile('readonly-write-child-clone.php');
$this->compile('readonly-write-clone-closure.php');
$this->compile('readonly-write-other-instance-clone.php');
}
public function testReadonlyPropertyCannotBeAssignedByReference(): void
@ -66,12 +33,16 @@ class ReadonlyPropertyTest extends \BaseTest
foreach ([
'readonly-write-compound.php',
'readonly-write-increment.php',
'readonly-write-decrement.php',
'readonly-write-subtract.php',
'readonly-write-concat.php',
'readonly-write-array-dim.php',
'readonly-write-array-index.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);
$this->exec('only supports direct assignment', $file);
}
}
}

@ -1095,11 +1095,6 @@ 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;
@ -1786,6 +1781,9 @@ class CompilerBase implements PropertyAccessContext
protected function detectClassOfExpr(NodeAbstract $expr): string
{
if ($expr instanceof Expr\Closure || $expr instanceof Expr\ArrowFunction) {
return 'Closure';
}
if ($expr instanceof Expr\MethodCall && $this->isNamedMethod($expr->name)) {
$keywordType = $this->findKeywordMethod($this->parseIdentifier($expr->name));
if ($keywordType !== null && $keywordType !== Type::OBJECT) {

@ -25,9 +25,6 @@ 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;

@ -212,7 +212,9 @@ trait AssignOpTrait
return $this->parseAssignToList($left, $right);
}
$propertyWriteTarget = $this->preparePropertyWriteTarget($left);
// A direct assignment to readonly must go through write_property so
// Zend can enforce scope, initialization state, type and clone rules.
$propertyWriteTarget = $this->preparePropertyWriteTarget($left, true);
$type = $this->detectTypeOfExpr($right);
$finalVarType = $this->getNormalAssignType($type);
$runtimeObjectAssignClass = '';
@ -433,6 +435,10 @@ trait AssignOpTrait
return false;
}
if ($def->isReadonly()) {
return true;
}
return !in_array($def->type, [Type::INT, Type::FLOAT, Type::BOOL, Type::STR, Type::ARRAY], true)
&& $rightType === Type::VAR;
}

@ -276,25 +276,6 @@ 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 . ')';
}
@ -322,15 +303,6 @@ 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);
}
@ -357,6 +329,16 @@ trait MethodCallTrait
}
}
if ($this->isNamedMethod($expr->name)
&& in_array(strtolower($expr->name->toString()), ['call', 'bind', 'bindto'], true)
&& strtolower(ltrim($class, '\\')) === 'closure') {
$closureMethod = $expr->name->toString();
$this->fatalError(
$expr,
'Closure::' . $closureMethod . '() is not supported'
);
}
$magicMethod = false;
$method = $this->identifierToStr($expr->name, literal: true);
@ -538,12 +520,6 @@ trait MethodCallTrait
}
}
private function isConstructorImplementationContext(): bool
{
return $this->isCurrentConstructor()
|| strtolower($this->methodDef?->traitMethod ?? '') === '__construct';
}
private function isDefinitelyObjectReceiver(
Expr $receiver,
string $object,
@ -607,23 +583,15 @@ 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');
}
if ($this->isNameExpr($expr->class)
&& $this->isIdExpr($expr->name)
&& strtolower(ltrim($expr->class->toString(), '\\')) === 'closure'
&& in_array(strtolower($expr->name->toString()), ['bind', 'bindto', 'call'], true)) {
$closureMethod = $expr->name->toString();
$this->fatalError(
$expr,
'Closure::' . $closureMethod . '() is not supported'
);
}
// parent::$method() still has a lexical parent class even when the

@ -207,7 +207,9 @@ trait PropertyAccessTrait
protected function emitDynamicPropertyFetchRef(Expr\PropertyFetch $expr, NodeAbstract $errorNode): string
{
$target = $this->preparePropertyWriteTarget($expr);
// Reference diagnostics are more specific than the generic readonly
// mutation error emitted by preparePropertyWriteTarget().
$target = $this->preparePropertyWriteTarget($expr, true);
if ($this->canEmitDynamicPropertyTarget($target)) {
$objectExpr = $target->getDynamicObjectExpr();
if (!$this->hasVar($objectExpr)) {
@ -443,7 +445,7 @@ trait PropertyAccessTrait
$this->assertCanAssignObjectProperty($left, $right, 'static property');
}
protected function preparePropertyWriteTarget(NodeAbstract $left, bool $checkReadonlyWrite = true): ?PropertyWriteTarget
protected function preparePropertyWriteTarget(NodeAbstract $left, bool $allowReadonlyAssignment = false): ?PropertyWriteTarget
{
if ($left instanceof Expr\PropertyFetch) {
$objectExpr = null;
@ -455,8 +457,8 @@ trait PropertyAccessTrait
if ($this->isIdExpr($left->name)) {
$this->getPropertyIdentifier($left, $left->var, $left->name);
$this->assertPropertySetVisibility($left);
if ($checkReadonlyWrite) {
$this->assertReadonlyPropertyWriteContext($left);
if (!$allowReadonlyAssignment) {
$this->assertReadonlyPropertyDirectAssignmentOnly($left);
}
}
return new PropertyWriteTarget($left, 'object property', $objectExpr, $propertyExpr);
@ -473,13 +475,7 @@ 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
private function assertReadonlyPropertyDirectAssignmentOnly(Expr\PropertyFetch $property): void
{
$access = $this->getNativePropertyAccess($property);
if ($access === null || !$access->getPropertyDef()->isReadonly()) {
@ -490,27 +486,7 @@ trait PropertyAccessTrait
$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`"
);
}
$this->fatalError($property, "Readonly property `{$display}` only supports direct assignment");
}
protected function assertReadonlyPropertyReferenceForbidden(
@ -806,7 +782,7 @@ trait PropertyAccessTrait
} elseif ($this->isPropertyFetch($var)) {
// unset has its own unconditional readonly diagnostic below;
// it is forbidden even while __construct is running.
$propertyWriteTarget = $this->preparePropertyWriteTarget($var, false);
$propertyWriteTarget = $this->preparePropertyWriteTarget($var, true);
$object = $this->getDynamicPropertyFetchObjectExpr($var, $propertyWriteTarget);
$restoreDefault = null;
if ($this->isIdExpr($var->name)) {

@ -4630,7 +4630,6 @@ 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;

@ -1,5 +1,5 @@
--TEST--
TypePHP readonly properties may be updated while cloning
readonly properties may be reinitialized once while cloning
--FILE--
<?php
@ -16,8 +16,7 @@ class ReadonlyCloneBase
public function __clone(): void
{
$this->base++;
$this->base += 3;
$this->base = 5;
}
}
@ -36,10 +35,13 @@ class ReadonlyCloneValue extends ReadonlyCloneBase
public function __clone(): void
{
parent::__clone();
$this->name = 'clone';
$this->name .= 'd';
$this->items[] = 2;
$this->items[0] = 10;
$this->name = 'cloned';
$this->items = [10, 2];
try {
$this->name = 'again';
} catch (Error $error) {
echo $error->getMessage(), "\n";
}
}
}
@ -52,6 +54,7 @@ function main(): void
}
?>
--EXPECT--
Cannot modify readonly property ReadonlyCloneValue::$name
int(1)
string(8) "original"
array(1) {

@ -1,42 +0,0 @@
--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)
}

@ -1,35 +0,0 @@
--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)

@ -1,30 +0,0 @@
--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)

@ -0,0 +1,82 @@
--TEST--
readonly properties use PHP one-time initialization semantics
--FILE--
<?php
class ReadonlyBase
{
public readonly int $fromMethod;
public readonly int $fromChild;
public readonly int $fromClosure;
public function initializeMethod(): void
{
$this->fromMethod = 10;
}
public function initializeClosure(): void
{
$writer = function (): void {
$this->fromClosure = 30;
};
$writer();
}
}
class ReadonlyChild extends ReadonlyBase
{
public function initializeChild(): void
{
$this->fromChild = 20;
}
}
class MutableValue
{
public int $number = 0;
}
class ReadonlyObjectHolder
{
public readonly MutableValue $value;
public function initialize(): void
{
$this->value = new MutableValue();
}
}
function main(): void
{
$value = new ReadonlyChild();
$value->initializeMethod();
$value->initializeChild();
$value->initializeClosure();
var_dump($value->fromMethod, $value->fromChild, $value->fromClosure);
try {
$value->initializeMethod();
} catch (Error $error) {
echo $error->getMessage(), "\n";
}
$external = new ReadonlyChild();
try {
$external->fromMethod = 40;
} catch (Error $error) {
echo $error->getMessage(), "\n";
}
$holder = new ReadonlyObjectHolder();
$holder->initialize();
$holder->value->number = 50;
var_dump($holder->value->number);
}
?>
--EXPECT--
int(10)
int(20)
int(30)
Cannot modify readonly property ReadonlyBase::$fromMethod
Cannot modify protected(set) readonly property ReadonlyBase::$fromMethod from global scope
int(50)
Loading…
Cancel
Save