Skip to content

Commit 24f2687

Browse files
committed
feat: allow hooks for backed readonly properties
1 parent 690cde6 commit 24f2687

21 files changed

+587
-40
lines changed

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ PHP NEWS
5353
evaluation) and GH-18464 (Recursion protection for deprecation constants not
5454
released on bailout). (DanielEScherzer and ilutov)
5555
. Fixed AST printing for immediately invoked Closure. (Dmitrii Derepko)
56+
. Property hooks are now allowed on backed readonly properties. ()
5657

5758
- Curl:
5859
. Added curl_multi_get_handles(). (timwolla)

UPGRADING

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ PHP 8.5 UPGRADE NOTES
144144
RFC: https://wiki.php.net/rfc/attributes-on-constants
145145
. The #[\Deprecated] attribute can now be used on constants.
146146
RFC: https://wiki.php.net/rfc/attributes-on-constants
147+
. Property hooks are now allowed on backed readonly properties.
148+
RFC: https://wiki.php.net/rfc/readonly_hooks
147149

148150
- Curl:
149151
. Added support for share handles that are persisted across multiple PHP

Zend/tests/property_hooks/gh15419_1.phpt

Lines changed: 0 additions & 12 deletions
This file was deleted.

Zend/tests/property_hooks/gh15419_2.phpt

Lines changed: 0 additions & 14 deletions
This file was deleted.

Zend/tests/property_hooks/readonly.phpt

Lines changed: 0 additions & 12 deletions
This file was deleted.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--TEST--
2+
Backed property in readonly class may have hooks
3+
--FILE--
4+
<?php
5+
6+
// readonly class
7+
readonly class Test {
8+
public int $prop {
9+
get => $this->prop;
10+
set => $value;
11+
}
12+
13+
public function __construct(int $v) {
14+
$this->prop = $v;
15+
}
16+
17+
public function set($v)
18+
{
19+
$this->prop = $v;
20+
}
21+
}
22+
23+
$t = new Test(42);
24+
var_dump($t->prop);
25+
try {
26+
$t->set(43);
27+
} catch (Error $e) {
28+
echo $e->getMessage(), "\n";
29+
}
30+
try {
31+
$t->prop = 43;
32+
} catch (Error $e) {
33+
echo $e->getMessage(), "\n";
34+
}
35+
var_dump($t->prop);
36+
?>
37+
--EXPECT--
38+
int(42)
39+
Cannot modify readonly property Test::$prop
40+
Cannot modify protected(set) readonly property Test::$prop from global scope
41+
int(42)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--TEST--
2+
Non-readonly class cannot extend readonly class
3+
--FILE--
4+
<?php
5+
6+
readonly class ParentClass {
7+
public int $prop;
8+
}
9+
10+
class Test extends ParentClass {
11+
public function __construct(
12+
public int $prop {
13+
get => $this->prop;
14+
set => $value;
15+
}
16+
) {}
17+
}
18+
19+
?>
20+
--EXPECTF--
21+
Fatal error: Non-readonly class Test cannot extend readonly class ParentClass in %s on line %d
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--TEST--
2+
Readonly class cannot extend non-readonly class
3+
--FILE--
4+
<?php
5+
6+
class ParentClass {
7+
public int $prop;
8+
}
9+
10+
readonly class Test extends ParentClass {
11+
public function __construct(
12+
public int $prop {
13+
get => $this->prop;
14+
set => $value;
15+
}
16+
) {}
17+
}
18+
19+
?>
20+
--EXPECTF--
21+
Fatal error: Readonly class Test cannot extend non-readonly class ParentClass in %s on line %d
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
--TEST--
2+
Backed promoted property in readonly class may have hooks
3+
--FILE--
4+
<?php
5+
6+
// readonly class, promoted
7+
readonly class Test {
8+
public function __construct(
9+
public int $prop {
10+
get => $this->prop;
11+
set => $value;
12+
}
13+
) {}
14+
15+
public function set($v)
16+
{
17+
$this->prop = $v;
18+
}
19+
}
20+
21+
$t = new Test(42);
22+
var_dump($t->prop);
23+
try {
24+
$t->set(43);
25+
} catch (Error $e) {
26+
echo $e->getMessage(), "\n";
27+
}
28+
try {
29+
$t->prop = 43;
30+
} catch (Error $e) {
31+
echo $e->getMessage(), "\n";
32+
}
33+
var_dump($t->prop);
34+
?>
35+
--EXPECT--
36+
int(42)
37+
Cannot modify readonly property Test::$prop
38+
Cannot modify protected(set) readonly property Test::$prop from global scope
39+
int(42)
40+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
--TEST--
2+
Virtual promoted property in readonly class cannot have hooks
3+
--FILE--
4+
<?php
5+
6+
readonly class Test {
7+
public function __construct(
8+
public int $prop {
9+
get => 42;
10+
}
11+
) {}
12+
}
13+
14+
?>
15+
--EXPECTF--
16+
Fatal error: Hooked virtual properties cannot be readonly in %s on line %d
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
--TEST--
2+
Readonly classes can be constructed via reflection by ORM
3+
--FILE--
4+
<?php
5+
6+
interface DbConnection {
7+
public function loadCategory(string $id): Category;
8+
}
9+
10+
class Category {
11+
public function __construct(public string $name) {}
12+
}
13+
14+
class MockDbConnection implements DbConnection {
15+
public function loadCategory(string $id): Category {
16+
echo "hit database\n";
17+
return new Category("Category {$id}");
18+
}
19+
}
20+
21+
readonly class Product
22+
{
23+
public function __construct(
24+
public string $name,
25+
public float $price,
26+
public Category $category,
27+
) {}
28+
}
29+
30+
readonly class LazyProduct extends Product
31+
{
32+
private DbConnection $dbApi;
33+
34+
private string $categoryId;
35+
36+
public Category $category {
37+
get {
38+
return $this->category ??= $this->dbApi->loadCategory($this->categoryId);
39+
}
40+
}
41+
}
42+
43+
$reflect = new ReflectionClass(LazyProduct::class);
44+
$product = $reflect->newInstanceWithoutConstructor();
45+
46+
$nameProperty = $reflect->getProperty('name');
47+
$nameProperty->setAccessible(true);
48+
$nameProperty->setValue($product, 'Iced Chocolate');
49+
50+
$priceProperty = $reflect->getProperty('price');
51+
$priceProperty->setAccessible(true);
52+
$priceProperty->setValue($product, 1.99);
53+
54+
$db = $reflect->getProperty('dbApi');
55+
$db->setAccessible(true);
56+
$db->setValue($product, new MockDbConnection());
57+
58+
$categoryId = $reflect->getProperty('categoryId');
59+
$categoryId->setAccessible(true);
60+
$categoryId->setValue($product, '42');
61+
62+
// lazy loading, hit db
63+
$category1 = $product->category;
64+
echo $category1->name . "\n";
65+
66+
// cached category returned
67+
$category2 = $product->category;
68+
echo $category2->name . "\n";
69+
70+
// same category instance returned
71+
var_dump($category1 === $category2);
72+
73+
// can't be wrong, huh?
74+
var_dump($product);
75+
76+
// cannot set twice
77+
try {
78+
$categoryId->setValue($product, '420');
79+
} catch (Error $e) {
80+
echo $e->getMessage(), "\n";
81+
}
82+
83+
?>
84+
--EXPECT--
85+
hit database
86+
Category 42
87+
Category 42
88+
bool(true)
89+
object(LazyProduct)#2 (5) {
90+
["name"]=>
91+
string(14) "Iced Chocolate"
92+
["price"]=>
93+
float(1.99)
94+
["category"]=>
95+
object(Category)#8 (1) {
96+
["name"]=>
97+
string(11) "Category 42"
98+
}
99+
["dbApi":"LazyProduct":private]=>
100+
object(MockDbConnection)#6 (0) {
101+
}
102+
["categoryId":"LazyProduct":private]=>
103+
string(2) "42"
104+
}
105+
Cannot modify readonly property LazyProduct::$categoryId

0 commit comments

Comments
 (0)