- 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 enforcementpull/48/head
parent
e5f61eabfe
commit
8118fbd3dc
29 changed files with 224 additions and 303 deletions
@ -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(); |
||||
} |
||||
} |
||||
@ -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; } |
||||
} |
||||
@ -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…
Reference in new issue