- 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