Skip to content

Commit

Permalink
Implement readonly properties
Browse files Browse the repository at this point in the history
Add support for readonly properties, for which only a single
initializing assignment from the declaring scope is allowed.

RFC: https://wiki.php.net/rfc/readonly_properties_v2

Closes GH-7089.
  • Loading branch information
nikic committed Jul 20, 2021
1 parent b382883 commit 6780aaa
Show file tree
Hide file tree
Showing 42 changed files with 1,118 additions and 40 deletions.
2 changes: 2 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ PHP 8.1 UPGRADE NOTES
RFC: https://wiki.php.net/rfc/pure-intersection-types
. Added support for the final modifier for class constants.
RFC: https://wiki.php.net/rfc/final_class_const
. Added support for readonly properties.
RFC: https://wiki.php.net/rfc/readonly_properties_v2

- Curl:
. Added CURLOPT_DOH_URL option.
Expand Down
3 changes: 3 additions & 0 deletions Zend/tests/grammar/semi_reserved_001.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class Obj
function array(){ echo __METHOD__, PHP_EOL; }
function print(){ echo __METHOD__, PHP_EOL; }
function echo(){ echo __METHOD__, PHP_EOL; }
function readonly(){ echo __METHOD__, PHP_EOL; }
function require(){ echo __METHOD__, PHP_EOL; }
function require_once(){ echo __METHOD__, PHP_EOL; }
function return(){ echo __METHOD__, PHP_EOL; }
Expand Down Expand Up @@ -125,6 +126,7 @@ $obj->throw();
$obj->array();
$obj->print();
$obj->echo();
$obj->readonly();
$obj->require();
$obj->require_once();
$obj->return();
Expand Down Expand Up @@ -205,6 +207,7 @@ Obj::throw
Obj::array
Obj::print
Obj::echo
Obj::readonly
Obj::require
Obj::require_once
Obj::return
Expand Down
29 changes: 29 additions & 0 deletions Zend/tests/readonly_props/by_ref_foreach.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
--TEST--
By-ref foreach over readonly property
--FILE--
<?php

class Test {
public readonly int $prop;

public function init() {
$this->prop = 1;
}
}

$test = new Test;

// Okay, as foreach skips over uninitialized properties.
foreach ($test as &$prop) {}

$test->init();

try {
foreach ($test as &$prop) {}
} catch (Error $e) {
echo $e->getMessage(), "\n";
}

?>
--EXPECT--
Cannot acquire reference to readonly property Test::$prop
122 changes: 122 additions & 0 deletions Zend/tests/readonly_props/cache_slot.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
--TEST--
Test interaction with cache slots
--FILE--
<?php

class Test {
public readonly string $prop;
public readonly array $prop2;
public readonly object $prop3;
public function setProp(string $prop) {
$this->prop = $prop;
}
public function initAndAppendProp2() {
$this->prop2 = [];
$this->prop2[] = 1;
}
public function initProp3() {
$this->prop3 = new stdClass;
$this->prop3->foo = 1;
}
public function replaceProp3() {
$ref =& $this->prop3;
$ref = new stdClass;
}
}

$test = new Test;
$test->setProp("a");
var_dump($test->prop);
try {
$test->setProp("b");
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
var_dump($test->prop);
echo "\n";

$test = new Test;
try {
$test->initAndAppendProp2();
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
$test->initAndAppendProp2();
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
var_dump($test->prop2);
echo "\n";

$test = new Test;
$test->initProp3();
$test->replaceProp3();
var_dump($test->prop3);
$test->replaceProp3();
var_dump($test->prop3);
echo "\n";

// Test variations using closure rebinding, so we have unknown property_info in JIT.
$test = new Test;
(function() { $this->prop2 = []; })->call($test);
$appendProp2 = (function() {
$this->prop2[] = 1;
})->bindTo($test, Test::class);
try {
$appendProp2();
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
$appendProp2();
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
var_dump($test->prop2);
echo "\n";

$test = new Test;
$replaceProp3 = (function() {
$ref =& $this->prop3;
$ref = new stdClass;
})->bindTo($test, Test::class);
$test->initProp3();
$replaceProp3();
var_dump($test->prop3);
$replaceProp3();
var_dump($test->prop3);

?>
--EXPECT--
string(1) "a"
Cannot modify readonly property Test::$prop
string(1) "a"

Cannot modify readonly property Test::$prop2
Cannot modify readonly property Test::$prop2
array(0) {
}

object(stdClass)#3 (1) {
["foo"]=>
int(1)
}
object(stdClass)#3 (1) {
["foo"]=>
int(1)
}

Cannot modify readonly property Test::$prop2
Cannot modify readonly property Test::$prop2
array(0) {
}

object(stdClass)#5 (1) {
["foo"]=>
int(1)
}
object(stdClass)#5 (1) {
["foo"]=>
int(1)
}
72 changes: 72 additions & 0 deletions Zend/tests/readonly_props/initialization_scope.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
--TEST--
Initialization can only happen from private scope
--FILE--
<?php

class A {
public readonly int $prop;

public function initPrivate() {
$this->prop = 3;
}
}
class B extends A {
public function initProtected() {
$this->prop = 2;
}
}

$test = new B;
try {
$test->prop = 1;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
$test->initProtected();
} catch (Error $e) {
echo $e->getMessage(), "\n";
}

$test->initPrivate();
var_dump($test->prop);

// Rebinding bypass works.
$test = new B;
(function() {
$this->prop = 1;
})->bindTo($test, A::class)();
var_dump($test->prop);

class C extends A {
public readonly int $prop;
}

$test = new C;
$test->initPrivate();
var_dump($test->prop);

class X {
public function initFromParent() {
$this->prop = 1;
}
}
class Y extends X {
public readonly int $prop;
}

$test = new Y;
try {
$test->initFromParent();
} catch (Error $e) {
echo $e->getMessage(), "\n";
}

?>
--EXPECT--
Cannot initialize readonly property A::$prop from global scope
Cannot initialize readonly property A::$prop from scope B
int(3)
int(1)
int(3)
Cannot initialize readonly property Y::$prop from scope X
74 changes: 74 additions & 0 deletions Zend/tests/readonly_props/magic_get_set.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
--TEST--
Interaction with magic get/set
--FILE--
<?php

class Test {
public readonly int $prop;

public function unsetProp() {
unset($this->prop);
}

public function __get($name) {
echo __METHOD__, "($name)\n";
return 1;
}

public function __set($name, $value) {
echo __METHOD__, "($name, $value)\n";
}

public function __unset($name) {
echo __METHOD__, "($name)\n";
}

public function __isset($name) {
echo __METHOD__, "($name)\n";
return true;
}
}

$test = new Test;

// The property is in uninitialized state, no magic methods should be invoked.
var_dump(isset($test->prop));
try {
var_dump($test->prop);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
$test->prop = 1;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
unset($test->prop);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}

$test->unsetProp();

var_dump(isset($test->prop));
var_dump($test->prop);
$test->prop = 2;
try {
unset($test->prop);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}

?>
--EXPECT--
bool(false)
Typed property Test::$prop must not be accessed before initialization
Cannot initialize readonly property Test::$prop from global scope
Cannot unset readonly property Test::$prop from global scope
Test::__isset(prop)
bool(true)
Test::__get(prop)
int(1)
Test::__set(prop, 2)
Test::__unset(prop)
26 changes: 26 additions & 0 deletions Zend/tests/readonly_props/override_with_attributes.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
--TEST--
Can override readonly property with attributes
--FILE--
<?php

#[Attribute]
class FooAttribute {}

class A {
public readonly int $prop;

public function __construct() {
$this->prop = 42;
}
}
class B extends A {
#[FooAttribute]
public readonly int $prop;
}

var_dump((new ReflectionProperty(B::class, 'prop'))->getAttributes()[0]->newInstance());

?>
--EXPECT--
object(FooAttribute)#1 (0) {
}

0 comments on commit 6780aaa

Please sign in to comment.