Skip to content

Commit 6840873

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

19 files changed

+529
-33
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.
Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,73 @@
11
--TEST--
2-
Hooked properties cannot be readonly
2+
Backed readonly property may have nice hooks
33
--FILE--
44
<?php
55

6-
class Test {
7-
public readonly int $prop { get; set; }
6+
// class readonly
7+
final readonly class Foo
8+
{
9+
public function __construct(
10+
public array $values {
11+
set(array $value) => array_map(strtoupper(...), $value);
12+
},
13+
) {}
814
}
915

16+
// property readonly
17+
final class Foo2
18+
{
19+
public function __construct(
20+
public readonly array $values {
21+
set(array $value) => array_map(strtoupper(...), $value);
22+
},
23+
) {}
24+
}
25+
26+
// redundant readonly
27+
final readonly class Foo3
28+
{
29+
public function __construct(
30+
public readonly array $values {
31+
set(array $value) => array_map(strtoupper(...), $value);
32+
get => $this->makeNicer($this->values);
33+
},
34+
) {}
35+
36+
public function makeNicer(array $entries): array
37+
{
38+
return array_map(
39+
fn($i, $entry) => $entry . strtoupper(['', 'r', 'st'][$i]), array_keys($entries),
40+
$entries
41+
);
42+
}
43+
}
44+
45+
\var_dump(new Foo(['yo,', 'you', 'can'])->values);
46+
\var_dump(new Foo2(['just', 'do', 'things'])->values);
47+
\var_dump(new Foo3(['nice', 'nice', 'nice'])->values);
1048
?>
11-
--EXPECTF--
12-
Fatal error: Hooked properties cannot be readonly in %s on line %d
49+
--EXPECT--
50+
array(3) {
51+
[0]=>
52+
string(3) "YO,"
53+
[1]=>
54+
string(3) "YOU"
55+
[2]=>
56+
string(3) "CAN"
57+
}
58+
array(3) {
59+
[0]=>
60+
string(4) "JUST"
61+
[1]=>
62+
string(2) "DO"
63+
[2]=>
64+
string(6) "THINGS"
65+
}
66+
array(3) {
67+
[0]=>
68+
string(4) "NICE"
69+
[1]=>
70+
string(5) "NICER"
71+
[2]=>
72+
string(6) "NICEST"
73+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--TEST--
2+
Backed property, readonly class
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+
Backed property cannot redeclare readonly as non-readonly property
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+
Backed property cannot redeclare non-readonly as readonly property
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 property, promoted, readonly class
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: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
--TEST--
2+
Readonly hooks may be lazy loaded
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 LazyProduct
22+
{
23+
private DbConnection $dbApi;
24+
private string $categoryId;
25+
26+
public Category $category {
27+
get {
28+
return $this->category ??= $this->dbApi->loadCategory($this->categoryId);
29+
}
30+
}
31+
}
32+
33+
$product = new LazyProduct();
34+
$reflect = new ReflectionClass($product);
35+
36+
$db = $reflect->getProperty('dbApi');
37+
$db->setAccessible(true);
38+
$db->setValue($product, new MockDbConnection());
39+
40+
$categoryId = $reflect->getProperty('categoryId');
41+
$categoryId->setAccessible(true);
42+
$categoryId->setValue($product, '42');
43+
44+
// lazy loading, hit db
45+
$category1 = $product->category;
46+
echo $category1->name . "\n";
47+
48+
// cached category returned
49+
$category2 = $product->category;
50+
echo $category2->name . "\n";
51+
52+
// same category instance returned
53+
var_dump($category1 === $category2);
54+
55+
// cannot set twice
56+
try {
57+
$categoryId->setValue($product, '420');
58+
} catch (Error $e) {
59+
echo $e->getMessage(), "\n";
60+
}
61+
62+
?>
63+
--EXPECT--
64+
hit database
65+
Category 42
66+
Category 42
67+
bool(true)
68+
Cannot modify readonly property LazyProduct::$categoryId
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--TEST--
2+
Backed readonly property may have hooks
3+
--FILE--
4+
<?php
5+
6+
// readonly property
7+
class Test {
8+
public readonly 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)

0 commit comments

Comments
 (0)