From 12b986aa5f8ea31076f6aefa8bc6c04efe2e054b Mon Sep 17 00:00:00 2001 From: NickSdot Date: Wed, 4 Jun 2025 17:31:20 +0800 Subject: [PATCH 01/13] feat: allow hooks for backed readonly properties --- NEWS | 1 + UPGRADING | 2 + Zend/tests/property_hooks/gh15419_1.phpt | 12 -- Zend/tests/property_hooks/gh15419_2.phpt | 14 --- Zend/tests/property_hooks/readonly.phpt | 12 -- .../readonly_class_property_backed.phpt | 41 +++++++ ...y_class_property_backed_inheritance_1.phpt | 21 ++++ ...y_class_property_backed_inheritance_2.phpt | 21 ++++ ...adonly_class_property_backed_promoted.phpt | 40 +++++++ ...donly_class_property_virtual_promoted.phpt | 16 +++ Zend/tests/property_hooks/readonly_lazy.phpt | 105 +++++++++++++++++ .../readonly_property_backed.phpt | 108 ++++++++++++++++++ ...eadonly_property_backed_inheritance_1.phpt | 21 ++++ ...eadonly_property_backed_inheritance_2.phpt | 21 ++++ ...eadonly_property_backed_inheritance_3.phpt | 97 ++++++++++++++++ .../readonly_property_backed_promoted.phpt | 39 +++++++ .../readonly_property_backed_trait_1.phpt | 16 +++ ...readonly_property_virtual_in_abstract.phpt | 11 ++ .../readonly_property_virtual_in_class.phpt | 13 +++ ...eadonly_property_virtual_in_interface.phpt | 11 ++ Zend/zend_compile.c | 5 +- 21 files changed, 587 insertions(+), 40 deletions(-) delete mode 100644 Zend/tests/property_hooks/gh15419_1.phpt delete mode 100644 Zend/tests/property_hooks/gh15419_2.phpt delete mode 100644 Zend/tests/property_hooks/readonly.phpt create mode 100644 Zend/tests/property_hooks/readonly_class_property_backed.phpt create mode 100644 Zend/tests/property_hooks/readonly_class_property_backed_inheritance_1.phpt create mode 100644 Zend/tests/property_hooks/readonly_class_property_backed_inheritance_2.phpt create mode 100644 Zend/tests/property_hooks/readonly_class_property_backed_promoted.phpt create mode 100644 Zend/tests/property_hooks/readonly_class_property_virtual_promoted.phpt create mode 100644 Zend/tests/property_hooks/readonly_lazy.phpt create mode 100644 Zend/tests/property_hooks/readonly_property_backed.phpt create mode 100644 Zend/tests/property_hooks/readonly_property_backed_inheritance_1.phpt create mode 100644 Zend/tests/property_hooks/readonly_property_backed_inheritance_2.phpt create mode 100644 Zend/tests/property_hooks/readonly_property_backed_inheritance_3.phpt create mode 100644 Zend/tests/property_hooks/readonly_property_backed_promoted.phpt create mode 100644 Zend/tests/property_hooks/readonly_property_backed_trait_1.phpt create mode 100644 Zend/tests/property_hooks/readonly_property_virtual_in_abstract.phpt create mode 100644 Zend/tests/property_hooks/readonly_property_virtual_in_class.phpt create mode 100644 Zend/tests/property_hooks/readonly_property_virtual_in_interface.phpt diff --git a/NEWS b/NEWS index 8427273c9d9a5..738054cb95921 100644 --- a/NEWS +++ b/NEWS @@ -53,6 +53,7 @@ PHP NEWS evaluation) and GH-18464 (Recursion protection for deprecation constants not released on bailout). (DanielEScherzer and ilutov) . Fixed AST printing for immediately invoked Closure. (Dmitrii Derepko) + . Property hooks are now allowed on backed readonly properties. (Crell, NickSdot and iluuu1994) - Curl: . Added curl_multi_get_handles(). (timwolla) diff --git a/UPGRADING b/UPGRADING index 8f8b7e7685e2a..ed9ece336939b 100644 --- a/UPGRADING +++ b/UPGRADING @@ -144,6 +144,8 @@ PHP 8.5 UPGRADE NOTES RFC: https://wiki.php.net/rfc/attributes-on-constants . The #[\Deprecated] attribute can now be used on constants. RFC: https://wiki.php.net/rfc/attributes-on-constants + . Property hooks are now allowed on backed readonly properties. + RFC: https://wiki.php.net/rfc/readonly_hooks - Curl: . Added support for share handles that are persisted across multiple PHP diff --git a/Zend/tests/property_hooks/gh15419_1.phpt b/Zend/tests/property_hooks/gh15419_1.phpt deleted file mode 100644 index 41a45154f1fde..0000000000000 --- a/Zend/tests/property_hooks/gh15419_1.phpt +++ /dev/null @@ -1,12 +0,0 @@ ---TEST-- -GH-15419: Readonly classes may not declare properties with hooks ---FILE-- - $value; } -} - -?> ---EXPECTF-- -Fatal error: Hooked properties cannot be readonly in %s on line %d diff --git a/Zend/tests/property_hooks/gh15419_2.phpt b/Zend/tests/property_hooks/gh15419_2.phpt deleted file mode 100644 index dfa6490fdc0cd..0000000000000 --- a/Zend/tests/property_hooks/gh15419_2.phpt +++ /dev/null @@ -1,14 +0,0 @@ ---TEST-- -GH-15419: Readonly classes may not declare promoted properties with hooks ---FILE-- - $value; }, - ) {} -} - -?> ---EXPECTF-- -Fatal error: Hooked properties cannot be readonly in %s on line %d diff --git a/Zend/tests/property_hooks/readonly.phpt b/Zend/tests/property_hooks/readonly.phpt deleted file mode 100644 index be68bc800576e..0000000000000 --- a/Zend/tests/property_hooks/readonly.phpt +++ /dev/null @@ -1,12 +0,0 @@ ---TEST-- -Hooked properties cannot be readonly ---FILE-- - ---EXPECTF-- -Fatal error: Hooked properties cannot be readonly in %s on line %d diff --git a/Zend/tests/property_hooks/readonly_class_property_backed.phpt b/Zend/tests/property_hooks/readonly_class_property_backed.phpt new file mode 100644 index 0000000000000..e4448ea6579a8 --- /dev/null +++ b/Zend/tests/property_hooks/readonly_class_property_backed.phpt @@ -0,0 +1,41 @@ +--TEST-- +Backed property in readonly class may have hooks +--FILE-- + $this->prop; + set => $value; + } + + public function __construct(int $v) { + $this->prop = $v; + } + + public function set($v) + { + $this->prop = $v; + } +} + +$t = new Test(42); +var_dump($t->prop); +try { + $t->set(43); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +try { + $t->prop = 43; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +var_dump($t->prop); +?> +--EXPECT-- +int(42) +Cannot modify readonly property Test::$prop +Cannot modify protected(set) readonly property Test::$prop from global scope +int(42) \ No newline at end of file diff --git a/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_1.phpt b/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_1.phpt new file mode 100644 index 0000000000000..d9201d977929b --- /dev/null +++ b/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_1.phpt @@ -0,0 +1,21 @@ +--TEST-- +Non-readonly class cannot extend readonly class +--FILE-- + $this->prop; + set => $value; + } + ) {} +} + +?> +--EXPECTF-- +Fatal error: Non-readonly class Test cannot extend readonly class ParentClass in %s on line %d \ No newline at end of file diff --git a/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_2.phpt b/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_2.phpt new file mode 100644 index 0000000000000..df62a53b8c097 --- /dev/null +++ b/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_2.phpt @@ -0,0 +1,21 @@ +--TEST-- +Readonly class cannot extend non-readonly class +--FILE-- + $this->prop; + set => $value; + } + ) {} +} + +?> +--EXPECTF-- +Fatal error: Readonly class Test cannot extend non-readonly class ParentClass in %s on line %d \ No newline at end of file diff --git a/Zend/tests/property_hooks/readonly_class_property_backed_promoted.phpt b/Zend/tests/property_hooks/readonly_class_property_backed_promoted.phpt new file mode 100644 index 0000000000000..83cfeb46062d7 --- /dev/null +++ b/Zend/tests/property_hooks/readonly_class_property_backed_promoted.phpt @@ -0,0 +1,40 @@ +--TEST-- +Backed promoted property in readonly class may have hooks +--FILE-- + $this->prop; + set => $value; + } + ) {} + + public function set($v) + { + $this->prop = $v; + } +} + +$t = new Test(42); +var_dump($t->prop); +try { + $t->set(43); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +try { + $t->prop = 43; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +var_dump($t->prop); +?> +--EXPECT-- +int(42) +Cannot modify readonly property Test::$prop +Cannot modify protected(set) readonly property Test::$prop from global scope +int(42) + diff --git a/Zend/tests/property_hooks/readonly_class_property_virtual_promoted.phpt b/Zend/tests/property_hooks/readonly_class_property_virtual_promoted.phpt new file mode 100644 index 0000000000000..c0756ec2b9b1f --- /dev/null +++ b/Zend/tests/property_hooks/readonly_class_property_virtual_promoted.phpt @@ -0,0 +1,16 @@ +--TEST-- +Virtual promoted property in readonly class cannot have hooks +--FILE-- + 42; + } + ) {} +} + +?> +--EXPECTF-- +Fatal error: Hooked virtual properties cannot be readonly in %s on line %d diff --git a/Zend/tests/property_hooks/readonly_lazy.phpt b/Zend/tests/property_hooks/readonly_lazy.phpt new file mode 100644 index 0000000000000..f3486cb63767a --- /dev/null +++ b/Zend/tests/property_hooks/readonly_lazy.phpt @@ -0,0 +1,105 @@ +--TEST-- +Readonly classes can be constructed via reflection by ORM +--FILE-- +category ??= $this->dbApi->loadCategory($this->categoryId); + } + } +} + +$reflect = new ReflectionClass(LazyProduct::class); +$product = $reflect->newInstanceWithoutConstructor(); + +$nameProperty = $reflect->getProperty('name'); +$nameProperty->setAccessible(true); +$nameProperty->setValue($product, 'Iced Chocolate'); + +$priceProperty = $reflect->getProperty('price'); +$priceProperty->setAccessible(true); +$priceProperty->setValue($product, 1.99); + +$db = $reflect->getProperty('dbApi'); +$db->setAccessible(true); +$db->setValue($product, new MockDbConnection()); + +$categoryId = $reflect->getProperty('categoryId'); +$categoryId->setAccessible(true); +$categoryId->setValue($product, '42'); + +// lazy loading, hit db +$category1 = $product->category; +echo $category1->name . "\n"; + +// cached category returned +$category2 = $product->category; +echo $category2->name . "\n"; + +// same category instance returned +var_dump($category1 === $category2); + +// can't be wrong, huh? +var_dump($product); + +// cannot set twice +try { + $categoryId->setValue($product, '420'); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +hit database +Category 42 +Category 42 +bool(true) +object(LazyProduct)#2 (5) { + ["name"]=> + string(14) "Iced Chocolate" + ["price"]=> + float(1.99) + ["category"]=> + object(Category)#8 (1) { + ["name"]=> + string(11) "Category 42" + } + ["dbApi":"LazyProduct":private]=> + object(MockDbConnection)#6 (0) { + } + ["categoryId":"LazyProduct":private]=> + string(2) "42" +} +Cannot modify readonly property LazyProduct::$categoryId diff --git a/Zend/tests/property_hooks/readonly_property_backed.phpt b/Zend/tests/property_hooks/readonly_property_backed.phpt new file mode 100644 index 0000000000000..e8b9eb50ee63d --- /dev/null +++ b/Zend/tests/property_hooks/readonly_property_backed.phpt @@ -0,0 +1,108 @@ +--TEST-- +Backed readonly property may have hooks +--FILE-- + $this->prop; + set => $value; + } + + public function __construct(int $v) { + $this->prop = $v; + } + + public function set($v) + { + $this->prop = $v; + } +} + +$t = new Test(42); +var_dump($t->prop); +try { + $t->set(43); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +try { + $t->prop = 43; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +var_dump($t->prop); + +// class readonly +final readonly class Foo +{ + public function __construct( + public array $values { + set(array $value) => array_map(strtoupper(...), $value); + }, + ) {} +} + +// property readonly +final class Foo2 +{ + public function __construct( + public readonly array $values { + set(array $value) => array_map(strtoupper(...), $value); + }, + ) {} +} + +// redundant readonly +final readonly class Foo3 +{ + public function __construct( + public readonly array $values { + set(array $value) => array_map(strtoupper(...), $value); + get => $this->makeNicer($this->values); + }, + ) {} + + public function makeNicer(array $entries): array + { + return array_map( + fn($i, $entry) => $entry . strtoupper(['', 'r', 'st'][$i]), array_keys($entries), + $entries + ); + } +} + +\var_dump(new Foo(['yo,', 'you', 'can'])->values); +\var_dump(new Foo2(['just', 'do', 'things'])->values); +\var_dump(new Foo3(['nice', 'nice', 'nice'])->values); +?> +--EXPECT-- +int(42) +Cannot modify readonly property Test::$prop +Cannot modify protected(set) readonly property Test::$prop from global scope +int(42) +array(3) { + [0]=> + string(3) "YO," + [1]=> + string(3) "YOU" + [2]=> + string(3) "CAN" +} +array(3) { + [0]=> + string(4) "JUST" + [1]=> + string(2) "DO" + [2]=> + string(6) "THINGS" +} +array(3) { + [0]=> + string(4) "NICE" + [1]=> + string(5) "NICER" + [2]=> + string(6) "NICEST" +} \ No newline at end of file diff --git a/Zend/tests/property_hooks/readonly_property_backed_inheritance_1.phpt b/Zend/tests/property_hooks/readonly_property_backed_inheritance_1.phpt new file mode 100644 index 0000000000000..49cf9f67bcc02 --- /dev/null +++ b/Zend/tests/property_hooks/readonly_property_backed_inheritance_1.phpt @@ -0,0 +1,21 @@ +--TEST-- +Backed property cannot redeclare readonly as non-readonly property +--FILE-- + $this->prop; + set => $value; + } + ) {} +} + +?> +--EXPECTF-- +Fatal error: Cannot redeclare readonly property ParentClass::$prop as non-readonly Test::$prop %s on line %d \ No newline at end of file diff --git a/Zend/tests/property_hooks/readonly_property_backed_inheritance_2.phpt b/Zend/tests/property_hooks/readonly_property_backed_inheritance_2.phpt new file mode 100644 index 0000000000000..6cb1ac8571b7d --- /dev/null +++ b/Zend/tests/property_hooks/readonly_property_backed_inheritance_2.phpt @@ -0,0 +1,21 @@ +--TEST-- +Backed property cannot redeclare non-readonly as readonly property +--FILE-- + $this->prop; + set => $value; + } + ) {} +} + +?> +--EXPECTF-- +Fatal error: Cannot redeclare non-readonly property ParentClass::$prop as readonly Test::$prop in %s on line %d \ No newline at end of file diff --git a/Zend/tests/property_hooks/readonly_property_backed_inheritance_3.phpt b/Zend/tests/property_hooks/readonly_property_backed_inheritance_3.phpt new file mode 100644 index 0000000000000..9165f87a4af4d --- /dev/null +++ b/Zend/tests/property_hooks/readonly_property_backed_inheritance_3.phpt @@ -0,0 +1,97 @@ +--TEST-- +Backed readonly property get() in child class behaves as expected +--FILE-- +prop}\n"; + var_dump($this); + return $this->prop; + } +} + +class ChildClass extends ParentClass { + + public readonly int $prop { + get { + echo 'In ChildClass::$prop::get():' . "\n"; + echo ' parent::$prop::get(): ' . parent::$prop::get() . "\n"; + echo ' $this->prop: ' . $this->prop . "\n"; + echo ' $this->prop * 2: ' . $this->prop * 2 . "\n"; + return $this->prop * 2; + } + set => $value; + } + + public function setAgain() { + $this->prop = 42; + } +} + +$t = new ChildClass(911); + +echo "\nFirst call:\n"; +$t->prop; + +echo "\nFirst call didn't change state:\n"; +$t->prop; + +echo "\nUnderlying value never touched:\n"; +var_dump($t); + +echo "\nCalling scope is child, hitting child get() and child state expected:\n"; +$t->getParentValue(); + +try { + $t->setAgain(); // cannot write, readonly +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +try { + $t->prop = 43; // cannot write, visibility +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +First call: +In ChildClass::$prop::get(): + parent::$prop::get(): 911 + $this->prop: 911 + $this->prop * 2: 1822 + +First call didn't change state: +In ChildClass::$prop::get(): + parent::$prop::get(): 911 + $this->prop: 911 + $this->prop * 2: 1822 + +Underlying value never touched: +object(ChildClass)#1 (1) { + ["prop"]=> + int(911) +} + +Calling scope is child, hitting child get() and child state expected: +In ChildClass::$prop::get(): + parent::$prop::get(): 911 + $this->prop: 911 + $this->prop * 2: 1822 +ParentClass::getParentValue(): 1822 +object(ChildClass)#1 (1) { + ["prop"]=> + int(911) +} +In ChildClass::$prop::get(): + parent::$prop::get(): 911 + $this->prop: 911 + $this->prop * 2: 1822 +Cannot modify readonly property ChildClass::$prop +Cannot modify protected(set) readonly property ChildClass::$prop from global scope diff --git a/Zend/tests/property_hooks/readonly_property_backed_promoted.phpt b/Zend/tests/property_hooks/readonly_property_backed_promoted.phpt new file mode 100644 index 0000000000000..7000c42a12400 --- /dev/null +++ b/Zend/tests/property_hooks/readonly_property_backed_promoted.phpt @@ -0,0 +1,39 @@ +--TEST-- +Backed promoted readonly property may have hooks +--FILE-- + $this->prop; + set => $value; + } + ) {} + + public function set($v) + { + $this->prop = $v; + } +} + +$t = new Test(42); +var_dump($t->prop); +try { + $t->set(43); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +try { + $t->prop = 43; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +var_dump($t->prop); +?> +--EXPECT-- +int(42) +Cannot modify readonly property Test::$prop +Cannot modify protected(set) readonly property Test::$prop from global scope +int(42) \ No newline at end of file diff --git a/Zend/tests/property_hooks/readonly_property_backed_trait_1.phpt b/Zend/tests/property_hooks/readonly_property_backed_trait_1.phpt new file mode 100644 index 0000000000000..7fc055c2bd309 --- /dev/null +++ b/Zend/tests/property_hooks/readonly_property_backed_trait_1.phpt @@ -0,0 +1,16 @@ +--TEST-- +Readonly class Test cannot use trait with a non-readonly property +--FILE-- + +--EXPECTF-- +Fatal error: Readonly class Test cannot use trait with a non-readonly property SomeTrait::$prop in %s on line %d diff --git a/Zend/tests/property_hooks/readonly_property_virtual_in_abstract.phpt b/Zend/tests/property_hooks/readonly_property_virtual_in_abstract.phpt new file mode 100644 index 0000000000000..cffa9dfac01c7 --- /dev/null +++ b/Zend/tests/property_hooks/readonly_property_virtual_in_abstract.phpt @@ -0,0 +1,11 @@ +--TEST-- +Virtual readonly property in interface throws +--FILE-- + +--EXPECTF-- +Fatal error: Hooked virtual properties cannot be readonly in %s on line %d diff --git a/Zend/tests/property_hooks/readonly_property_virtual_in_class.phpt b/Zend/tests/property_hooks/readonly_property_virtual_in_class.phpt new file mode 100644 index 0000000000000..98d8b38e1d846 --- /dev/null +++ b/Zend/tests/property_hooks/readonly_property_virtual_in_class.phpt @@ -0,0 +1,13 @@ +--TEST-- +Virtual readonly property in class throws +--FILE-- + 42; + } +} +?> +--EXPECTF-- +Fatal error: Hooked virtual properties cannot be readonly in %s on line %d diff --git a/Zend/tests/property_hooks/readonly_property_virtual_in_interface.phpt b/Zend/tests/property_hooks/readonly_property_virtual_in_interface.phpt new file mode 100644 index 0000000000000..54cca055f0209 --- /dev/null +++ b/Zend/tests/property_hooks/readonly_property_virtual_in_interface.phpt @@ -0,0 +1,11 @@ +--TEST-- +Virtual readonly property in interface throws +--FILE-- + +--EXPECTF-- +Fatal error: Hooked virtual properties cannot be readonly in %s on line %d diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 0669d106f15e9..ccedc9cce7e28 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -8497,8 +8497,9 @@ static void zend_compile_property_hooks( { zend_class_entry *ce = CG(active_class_entry); - if (prop_info->flags & ZEND_ACC_READONLY) { - zend_error_noreturn(E_COMPILE_ERROR, "Hooked properties cannot be readonly"); + /* Allow hooks on backed readonly properties only. */ + if ((prop_info->flags & (ZEND_ACC_READONLY|ZEND_ACC_VIRTUAL)) == (ZEND_ACC_READONLY|ZEND_ACC_VIRTUAL)) { + zend_error_noreturn(E_COMPILE_ERROR, "Hooked virtual properties cannot be readonly"); } if (hooks->children == 0) { From 8c177da123e1beabbe8161be7196b4996f663f8a Mon Sep 17 00:00:00 2001 From: NickSdot Date: Sun, 8 Jun 2025 20:11:08 +0700 Subject: [PATCH 02/13] refactor: added tests for rfc examples --- ...=> readonly_rfc_example_lazy_product.phpt} | 0 .../readonly_rfc_example_validation.phpt | 32 +++++++++++++++++++ 2 files changed, 32 insertions(+) rename Zend/tests/property_hooks/{readonly_lazy.phpt => readonly_rfc_example_lazy_product.phpt} (100%) create mode 100644 Zend/tests/property_hooks/readonly_rfc_example_validation.phpt diff --git a/Zend/tests/property_hooks/readonly_lazy.phpt b/Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt similarity index 100% rename from Zend/tests/property_hooks/readonly_lazy.phpt rename to Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt diff --git a/Zend/tests/property_hooks/readonly_rfc_example_validation.phpt b/Zend/tests/property_hooks/readonly_rfc_example_validation.phpt new file mode 100644 index 0000000000000..ac3429ae7e111 --- /dev/null +++ b/Zend/tests/property_hooks/readonly_rfc_example_validation.phpt @@ -0,0 +1,32 @@ +--TEST-- +Readonly property hook validation +--FILE-- + $value > 0 ? $value : throw new \Error('Value must be greater 0'); }, + public int $y { set => $value > 0 ? $value : throw new \Error('Value must be greater 0'); }, + ) {} +} + +$one = new PositivePoint(1,1); +var_dump($one); + +try { + $two = new PositivePoint(0,1); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + + +?> +--EXPECTF-- +object(PositivePoint)#1 (2) { + ["x"]=> + int(1) + ["y"]=> + int(1) +} +Value must be greater 0 From c9ebae47e44d969e872bfb0fb7fbd1cf322fc93c Mon Sep 17 00:00:00 2001 From: NickSdot Date: Sat, 14 Jun 2025 10:58:56 +0700 Subject: [PATCH 03/13] refactor: added trailing newlines at EOF in tests --- Zend/tests/property_hooks/readonly_class_property_backed.phpt | 2 +- .../readonly_class_property_backed_inheritance_1.phpt | 2 +- .../readonly_class_property_backed_inheritance_2.phpt | 2 +- .../property_hooks/readonly_class_property_backed_promoted.phpt | 1 - Zend/tests/property_hooks/readonly_property_backed.phpt | 2 +- .../property_hooks/readonly_property_backed_inheritance_1.phpt | 2 +- .../tests/property_hooks/readonly_property_backed_promoted.phpt | 2 +- 7 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Zend/tests/property_hooks/readonly_class_property_backed.phpt b/Zend/tests/property_hooks/readonly_class_property_backed.phpt index e4448ea6579a8..44ffdacf49528 100644 --- a/Zend/tests/property_hooks/readonly_class_property_backed.phpt +++ b/Zend/tests/property_hooks/readonly_class_property_backed.phpt @@ -38,4 +38,4 @@ var_dump($t->prop); int(42) Cannot modify readonly property Test::$prop Cannot modify protected(set) readonly property Test::$prop from global scope -int(42) \ No newline at end of file +int(42) diff --git a/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_1.phpt b/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_1.phpt index d9201d977929b..95aa21f06b68b 100644 --- a/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_1.phpt +++ b/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_1.phpt @@ -18,4 +18,4 @@ class Test extends ParentClass { ?> --EXPECTF-- -Fatal error: Non-readonly class Test cannot extend readonly class ParentClass in %s on line %d \ No newline at end of file +Fatal error: Non-readonly class Test cannot extend readonly class ParentClass in %s on line %d diff --git a/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_2.phpt b/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_2.phpt index df62a53b8c097..ffac06a16ac13 100644 --- a/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_2.phpt +++ b/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_2.phpt @@ -18,4 +18,4 @@ readonly class Test extends ParentClass { ?> --EXPECTF-- -Fatal error: Readonly class Test cannot extend non-readonly class ParentClass in %s on line %d \ No newline at end of file +Fatal error: Readonly class Test cannot extend non-readonly class ParentClass in %s on line %d diff --git a/Zend/tests/property_hooks/readonly_class_property_backed_promoted.phpt b/Zend/tests/property_hooks/readonly_class_property_backed_promoted.phpt index 83cfeb46062d7..d42efa1815aac 100644 --- a/Zend/tests/property_hooks/readonly_class_property_backed_promoted.phpt +++ b/Zend/tests/property_hooks/readonly_class_property_backed_promoted.phpt @@ -37,4 +37,3 @@ int(42) Cannot modify readonly property Test::$prop Cannot modify protected(set) readonly property Test::$prop from global scope int(42) - diff --git a/Zend/tests/property_hooks/readonly_property_backed.phpt b/Zend/tests/property_hooks/readonly_property_backed.phpt index e8b9eb50ee63d..09784013e6691 100644 --- a/Zend/tests/property_hooks/readonly_property_backed.phpt +++ b/Zend/tests/property_hooks/readonly_property_backed.phpt @@ -105,4 +105,4 @@ array(3) { string(5) "NICER" [2]=> string(6) "NICEST" -} \ No newline at end of file +} diff --git a/Zend/tests/property_hooks/readonly_property_backed_inheritance_1.phpt b/Zend/tests/property_hooks/readonly_property_backed_inheritance_1.phpt index 49cf9f67bcc02..915c93656bbbf 100644 --- a/Zend/tests/property_hooks/readonly_property_backed_inheritance_1.phpt +++ b/Zend/tests/property_hooks/readonly_property_backed_inheritance_1.phpt @@ -18,4 +18,4 @@ class Test extends ParentClass { ?> --EXPECTF-- -Fatal error: Cannot redeclare readonly property ParentClass::$prop as non-readonly Test::$prop %s on line %d \ No newline at end of file +Fatal error: Cannot redeclare readonly property ParentClass::$prop as non-readonly Test::$prop %s on line %d diff --git a/Zend/tests/property_hooks/readonly_property_backed_promoted.phpt b/Zend/tests/property_hooks/readonly_property_backed_promoted.phpt index 7000c42a12400..740ea2d2dffd7 100644 --- a/Zend/tests/property_hooks/readonly_property_backed_promoted.phpt +++ b/Zend/tests/property_hooks/readonly_property_backed_promoted.phpt @@ -36,4 +36,4 @@ var_dump($t->prop); int(42) Cannot modify readonly property Test::$prop Cannot modify protected(set) readonly property Test::$prop from global scope -int(42) \ No newline at end of file +int(42) From b4df02dec728e626b486b0d34438d9b6cd902c8a Mon Sep 17 00:00:00 2001 From: NickSdot Date: Sat, 14 Jun 2025 11:28:48 +0700 Subject: [PATCH 04/13] refactor: adjusted test error message formatting see: https://github.com/php/php-src/pull/18757#discussion_r2135855772 --- .../property_hooks/readonly_class_property_backed.phpt | 8 ++++---- .../readonly_class_property_backed_promoted.phpt | 8 ++++---- Zend/tests/property_hooks/readonly_property_backed.phpt | 8 ++++---- .../readonly_property_backed_inheritance_3.phpt | 8 ++++---- .../property_hooks/readonly_property_backed_promoted.phpt | 8 ++++---- .../property_hooks/readonly_rfc_example_lazy_product.phpt | 4 ++-- .../property_hooks/readonly_rfc_example_validation.phpt | 4 ++-- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Zend/tests/property_hooks/readonly_class_property_backed.phpt b/Zend/tests/property_hooks/readonly_class_property_backed.phpt index 44ffdacf49528..eab7079eec470 100644 --- a/Zend/tests/property_hooks/readonly_class_property_backed.phpt +++ b/Zend/tests/property_hooks/readonly_class_property_backed.phpt @@ -25,17 +25,17 @@ var_dump($t->prop); try { $t->set(43); } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } try { $t->prop = 43; } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } var_dump($t->prop); ?> --EXPECT-- int(42) -Cannot modify readonly property Test::$prop -Cannot modify protected(set) readonly property Test::$prop from global scope +Error: Cannot modify readonly property Test::$prop +Error: Cannot modify protected(set) readonly property Test::$prop from global scope int(42) diff --git a/Zend/tests/property_hooks/readonly_class_property_backed_promoted.phpt b/Zend/tests/property_hooks/readonly_class_property_backed_promoted.phpt index d42efa1815aac..54a2529128571 100644 --- a/Zend/tests/property_hooks/readonly_class_property_backed_promoted.phpt +++ b/Zend/tests/property_hooks/readonly_class_property_backed_promoted.phpt @@ -23,17 +23,17 @@ var_dump($t->prop); try { $t->set(43); } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } try { $t->prop = 43; } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } var_dump($t->prop); ?> --EXPECT-- int(42) -Cannot modify readonly property Test::$prop -Cannot modify protected(set) readonly property Test::$prop from global scope +Error: Cannot modify readonly property Test::$prop +Error: Cannot modify protected(set) readonly property Test::$prop from global scope int(42) diff --git a/Zend/tests/property_hooks/readonly_property_backed.phpt b/Zend/tests/property_hooks/readonly_property_backed.phpt index 09784013e6691..9ea8a955a0892 100644 --- a/Zend/tests/property_hooks/readonly_property_backed.phpt +++ b/Zend/tests/property_hooks/readonly_property_backed.phpt @@ -25,12 +25,12 @@ var_dump($t->prop); try { $t->set(43); } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } try { $t->prop = 43; } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } var_dump($t->prop); @@ -79,8 +79,8 @@ final readonly class Foo3 ?> --EXPECT-- int(42) -Cannot modify readonly property Test::$prop -Cannot modify protected(set) readonly property Test::$prop from global scope +Error: Cannot modify readonly property Test::$prop +Error: Cannot modify protected(set) readonly property Test::$prop from global scope int(42) array(3) { [0]=> diff --git a/Zend/tests/property_hooks/readonly_property_backed_inheritance_3.phpt b/Zend/tests/property_hooks/readonly_property_backed_inheritance_3.phpt index 9165f87a4af4d..4959b202f669f 100644 --- a/Zend/tests/property_hooks/readonly_property_backed_inheritance_3.phpt +++ b/Zend/tests/property_hooks/readonly_property_backed_inheritance_3.phpt @@ -50,13 +50,13 @@ $t->getParentValue(); try { $t->setAgain(); // cannot write, readonly } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } try { $t->prop = 43; // cannot write, visibility } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } ?> @@ -93,5 +93,5 @@ In ChildClass::$prop::get(): parent::$prop::get(): 911 $this->prop: 911 $this->prop * 2: 1822 -Cannot modify readonly property ChildClass::$prop -Cannot modify protected(set) readonly property ChildClass::$prop from global scope +Error: Cannot modify readonly property ChildClass::$prop +Error: Cannot modify protected(set) readonly property ChildClass::$prop from global scope diff --git a/Zend/tests/property_hooks/readonly_property_backed_promoted.phpt b/Zend/tests/property_hooks/readonly_property_backed_promoted.phpt index 740ea2d2dffd7..8ec3a3f2fc9e9 100644 --- a/Zend/tests/property_hooks/readonly_property_backed_promoted.phpt +++ b/Zend/tests/property_hooks/readonly_property_backed_promoted.phpt @@ -23,17 +23,17 @@ var_dump($t->prop); try { $t->set(43); } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } try { $t->prop = 43; } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } var_dump($t->prop); ?> --EXPECT-- int(42) -Cannot modify readonly property Test::$prop -Cannot modify protected(set) readonly property Test::$prop from global scope +Error: Cannot modify readonly property Test::$prop +Error: Cannot modify protected(set) readonly property Test::$prop from global scope int(42) diff --git a/Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt b/Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt index f3486cb63767a..4bc1c66806348 100644 --- a/Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt +++ b/Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt @@ -77,7 +77,7 @@ var_dump($product); try { $categoryId->setValue($product, '420'); } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } ?> @@ -102,4 +102,4 @@ object(LazyProduct)#2 (5) { ["categoryId":"LazyProduct":private]=> string(2) "42" } -Cannot modify readonly property LazyProduct::$categoryId +Error: Cannot modify readonly property LazyProduct::$categoryId diff --git a/Zend/tests/property_hooks/readonly_rfc_example_validation.phpt b/Zend/tests/property_hooks/readonly_rfc_example_validation.phpt index ac3429ae7e111..2f0f95c8d34e3 100644 --- a/Zend/tests/property_hooks/readonly_rfc_example_validation.phpt +++ b/Zend/tests/property_hooks/readonly_rfc_example_validation.phpt @@ -17,7 +17,7 @@ var_dump($one); try { $two = new PositivePoint(0,1); } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } @@ -29,4 +29,4 @@ object(PositivePoint)#1 (2) { ["y"]=> int(1) } -Value must be greater 0 +Error: Value must be greater 0 From 92d70bcd6af0908d24a92e0947c50d4df0b6c4f2 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Sat, 14 Jun 2025 15:22:23 +0700 Subject: [PATCH 05/13] refactor: improved misleading error handling --- .../readonly_class_property_virtual_promoted.phpt | 2 +- .../readonly_property_virtual_in_abstract.phpt | 4 ++-- .../readonly_property_virtual_in_class.phpt | 2 +- .../readonly_property_virtual_in_interface.phpt | 4 ++-- Zend/zend_compile.c | 11 ++++++++++- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/Zend/tests/property_hooks/readonly_class_property_virtual_promoted.phpt b/Zend/tests/property_hooks/readonly_class_property_virtual_promoted.phpt index c0756ec2b9b1f..cc67d6fc12d6c 100644 --- a/Zend/tests/property_hooks/readonly_class_property_virtual_promoted.phpt +++ b/Zend/tests/property_hooks/readonly_class_property_virtual_promoted.phpt @@ -13,4 +13,4 @@ readonly class Test { ?> --EXPECTF-- -Fatal error: Hooked virtual properties cannot be readonly in %s on line %d +Fatal error: Hooked virtual properties may not be declared readonly in %s on line %d diff --git a/Zend/tests/property_hooks/readonly_property_virtual_in_abstract.phpt b/Zend/tests/property_hooks/readonly_property_virtual_in_abstract.phpt index cffa9dfac01c7..bbb4d52299d0b 100644 --- a/Zend/tests/property_hooks/readonly_property_virtual_in_abstract.phpt +++ b/Zend/tests/property_hooks/readonly_property_virtual_in_abstract.phpt @@ -1,5 +1,5 @@ --TEST-- -Virtual readonly property in interface throws +Hooked properties in abstract classes cannot be readonly --FILE-- --EXPECTF-- -Fatal error: Hooked virtual properties cannot be readonly in %s on line %d +Fatal error: Hooked properties in abstract classes may not be declared readonly in %s on line %d diff --git a/Zend/tests/property_hooks/readonly_property_virtual_in_class.phpt b/Zend/tests/property_hooks/readonly_property_virtual_in_class.phpt index 98d8b38e1d846..5fe63a4875566 100644 --- a/Zend/tests/property_hooks/readonly_property_virtual_in_class.phpt +++ b/Zend/tests/property_hooks/readonly_property_virtual_in_class.phpt @@ -10,4 +10,4 @@ class Test { } ?> --EXPECTF-- -Fatal error: Hooked virtual properties cannot be readonly in %s on line %d +Fatal error: Hooked virtual properties may not be declared readonly in %s on line %d diff --git a/Zend/tests/property_hooks/readonly_property_virtual_in_interface.phpt b/Zend/tests/property_hooks/readonly_property_virtual_in_interface.phpt index 54cca055f0209..3380f25a32af9 100644 --- a/Zend/tests/property_hooks/readonly_property_virtual_in_interface.phpt +++ b/Zend/tests/property_hooks/readonly_property_virtual_in_interface.phpt @@ -1,5 +1,5 @@ --TEST-- -Virtual readonly property in interface throws +Interface properties cannot be readonly --FILE-- --EXPECTF-- -Fatal error: Hooked virtual properties cannot be readonly in %s on line %d +Fatal error: Interface properties may not be declared readonly in %s on line %d diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index ccedc9cce7e28..bf5a461fb5fda 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -8499,7 +8499,16 @@ static void zend_compile_property_hooks( /* Allow hooks on backed readonly properties only. */ if ((prop_info->flags & (ZEND_ACC_READONLY|ZEND_ACC_VIRTUAL)) == (ZEND_ACC_READONLY|ZEND_ACC_VIRTUAL)) { - zend_error_noreturn(E_COMPILE_ERROR, "Hooked virtual properties cannot be readonly"); + + if (ce->ce_flags & ZEND_ACC_INTERFACE) { + zend_error_noreturn(E_COMPILE_ERROR, "Interface properties may not be declared readonly"); + } + + if (ce->ce_flags & (ZEND_ACC_IMPLICIT_ABSTRACT_CLASS|ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)) { + zend_error_noreturn(E_COMPILE_ERROR, "Hooked properties in abstract classes may not be declared readonly"); + } + + zend_error_noreturn(E_COMPILE_ERROR, "Hooked virtual properties may not be declared readonly"); } if (hooks->children == 0) { From 43f3130194fa4997fd4c11bb706bca7e16d878cd Mon Sep 17 00:00:00 2001 From: NickSdot Date: Sat, 14 Jun 2025 15:37:52 +0700 Subject: [PATCH 06/13] chore: removed unrelated tests --- ...y_class_property_backed_inheritance_1.phpt | 21 ------------------- ...y_class_property_backed_inheritance_2.phpt | 21 ------------------- .../readonly_property_backed_trait_1.phpt | 16 -------------- 3 files changed, 58 deletions(-) delete mode 100644 Zend/tests/property_hooks/readonly_class_property_backed_inheritance_1.phpt delete mode 100644 Zend/tests/property_hooks/readonly_class_property_backed_inheritance_2.phpt delete mode 100644 Zend/tests/property_hooks/readonly_property_backed_trait_1.phpt diff --git a/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_1.phpt b/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_1.phpt deleted file mode 100644 index 95aa21f06b68b..0000000000000 --- a/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_1.phpt +++ /dev/null @@ -1,21 +0,0 @@ ---TEST-- -Non-readonly class cannot extend readonly class ---FILE-- - $this->prop; - set => $value; - } - ) {} -} - -?> ---EXPECTF-- -Fatal error: Non-readonly class Test cannot extend readonly class ParentClass in %s on line %d diff --git a/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_2.phpt b/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_2.phpt deleted file mode 100644 index ffac06a16ac13..0000000000000 --- a/Zend/tests/property_hooks/readonly_class_property_backed_inheritance_2.phpt +++ /dev/null @@ -1,21 +0,0 @@ ---TEST-- -Readonly class cannot extend non-readonly class ---FILE-- - $this->prop; - set => $value; - } - ) {} -} - -?> ---EXPECTF-- -Fatal error: Readonly class Test cannot extend non-readonly class ParentClass in %s on line %d diff --git a/Zend/tests/property_hooks/readonly_property_backed_trait_1.phpt b/Zend/tests/property_hooks/readonly_property_backed_trait_1.phpt deleted file mode 100644 index 7fc055c2bd309..0000000000000 --- a/Zend/tests/property_hooks/readonly_property_backed_trait_1.phpt +++ /dev/null @@ -1,16 +0,0 @@ ---TEST-- -Readonly class Test cannot use trait with a non-readonly property ---FILE-- - ---EXPECTF-- -Fatal error: Readonly class Test cannot use trait with a non-readonly property SomeTrait::$prop in %s on line %d From 29520ede1b60a6e8cb11afd6f9b7672f43d6f464 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Sat, 14 Jun 2025 15:47:06 +0700 Subject: [PATCH 07/13] refactor: added trailing newline at EOF in tests --- .../property_hooks/readonly_property_backed_inheritance_2.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Zend/tests/property_hooks/readonly_property_backed_inheritance_2.phpt b/Zend/tests/property_hooks/readonly_property_backed_inheritance_2.phpt index 6cb1ac8571b7d..2c45f88056331 100644 --- a/Zend/tests/property_hooks/readonly_property_backed_inheritance_2.phpt +++ b/Zend/tests/property_hooks/readonly_property_backed_inheritance_2.phpt @@ -18,4 +18,4 @@ class Test extends ParentClass { ?> --EXPECTF-- -Fatal error: Cannot redeclare non-readonly property ParentClass::$prop as readonly Test::$prop in %s on line %d \ No newline at end of file +Fatal error: Cannot redeclare non-readonly property ParentClass::$prop as readonly Test::$prop in %s on line %d From bfee62df40120a0661e3d9ec0950e9f09c73e14c Mon Sep 17 00:00:00 2001 From: NickSdot Date: Sat, 14 Jun 2025 20:03:07 +0700 Subject: [PATCH 08/13] refactor: renamed "may not" back to "cannot", because it's more commonly used --- .../readonly_class_property_virtual_promoted.phpt | 2 +- .../readonly_property_virtual_in_abstract.phpt | 2 +- .../property_hooks/readonly_property_virtual_in_class.phpt | 2 +- .../readonly_property_virtual_in_interface.phpt | 2 +- Zend/zend_compile.c | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Zend/tests/property_hooks/readonly_class_property_virtual_promoted.phpt b/Zend/tests/property_hooks/readonly_class_property_virtual_promoted.phpt index cc67d6fc12d6c..e42a46747a2f9 100644 --- a/Zend/tests/property_hooks/readonly_class_property_virtual_promoted.phpt +++ b/Zend/tests/property_hooks/readonly_class_property_virtual_promoted.phpt @@ -13,4 +13,4 @@ readonly class Test { ?> --EXPECTF-- -Fatal error: Hooked virtual properties may not be declared readonly in %s on line %d +Fatal error: Hooked virtual properties cannot be declared readonly in %s on line %d diff --git a/Zend/tests/property_hooks/readonly_property_virtual_in_abstract.phpt b/Zend/tests/property_hooks/readonly_property_virtual_in_abstract.phpt index bbb4d52299d0b..7f6bdf4e09f3a 100644 --- a/Zend/tests/property_hooks/readonly_property_virtual_in_abstract.phpt +++ b/Zend/tests/property_hooks/readonly_property_virtual_in_abstract.phpt @@ -8,4 +8,4 @@ abstract class Test { } ?> --EXPECTF-- -Fatal error: Hooked properties in abstract classes may not be declared readonly in %s on line %d +Fatal error: Hooked properties in abstract classes cannot be declared readonly in %s on line %d diff --git a/Zend/tests/property_hooks/readonly_property_virtual_in_class.phpt b/Zend/tests/property_hooks/readonly_property_virtual_in_class.phpt index 5fe63a4875566..3f3cc88f360ec 100644 --- a/Zend/tests/property_hooks/readonly_property_virtual_in_class.phpt +++ b/Zend/tests/property_hooks/readonly_property_virtual_in_class.phpt @@ -10,4 +10,4 @@ class Test { } ?> --EXPECTF-- -Fatal error: Hooked virtual properties may not be declared readonly in %s on line %d +Fatal error: Hooked virtual properties cannot be declared readonly in %s on line %d diff --git a/Zend/tests/property_hooks/readonly_property_virtual_in_interface.phpt b/Zend/tests/property_hooks/readonly_property_virtual_in_interface.phpt index 3380f25a32af9..63923750febb5 100644 --- a/Zend/tests/property_hooks/readonly_property_virtual_in_interface.phpt +++ b/Zend/tests/property_hooks/readonly_property_virtual_in_interface.phpt @@ -8,4 +8,4 @@ interface Test { } ?> --EXPECTF-- -Fatal error: Interface properties may not be declared readonly in %s on line %d +Fatal error: Interface properties cannot be declared readonly in %s on line %d diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index bf5a461fb5fda..73967e544a880 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -8501,14 +8501,14 @@ static void zend_compile_property_hooks( if ((prop_info->flags & (ZEND_ACC_READONLY|ZEND_ACC_VIRTUAL)) == (ZEND_ACC_READONLY|ZEND_ACC_VIRTUAL)) { if (ce->ce_flags & ZEND_ACC_INTERFACE) { - zend_error_noreturn(E_COMPILE_ERROR, "Interface properties may not be declared readonly"); + zend_error_noreturn(E_COMPILE_ERROR, "Interface properties cannot be declared readonly"); } if (ce->ce_flags & (ZEND_ACC_IMPLICIT_ABSTRACT_CLASS|ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)) { - zend_error_noreturn(E_COMPILE_ERROR, "Hooked properties in abstract classes may not be declared readonly"); + zend_error_noreturn(E_COMPILE_ERROR, "Hooked properties in abstract classes cannot be declared readonly"); } - zend_error_noreturn(E_COMPILE_ERROR, "Hooked virtual properties may not be declared readonly"); + zend_error_noreturn(E_COMPILE_ERROR, "Hooked virtual properties cannot be declared readonly"); } if (hooks->children == 0) { From d7bf39b663057976ed3e8b2b797ed32740d35b1c Mon Sep 17 00:00:00 2001 From: NickSdot Date: Sun, 22 Jun 2025 22:50:47 +0700 Subject: [PATCH 09/13] formatting --- .../property_hooks/readonly_rfc_example_validation.phpt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Zend/tests/property_hooks/readonly_rfc_example_validation.phpt b/Zend/tests/property_hooks/readonly_rfc_example_validation.phpt index 2f0f95c8d34e3..32c1bf0d0cd04 100644 --- a/Zend/tests/property_hooks/readonly_rfc_example_validation.phpt +++ b/Zend/tests/property_hooks/readonly_rfc_example_validation.phpt @@ -5,10 +5,10 @@ Readonly property hook validation readonly class PositivePoint { - public function __construct( - public int $x { set => $value > 0 ? $value : throw new \Error('Value must be greater 0'); }, - public int $y { set => $value > 0 ? $value : throw new \Error('Value must be greater 0'); }, - ) {} + public function __construct( + public int $x { set => $value > 0 ? $value : throw new \Error('Value must be greater 0'); }, + public int $y { set => $value > 0 ? $value : throw new \Error('Value must be greater 0'); }, + ) {} } $one = new PositivePoint(1,1); From 71474c37390274ba0699e1b1db5f6d329db1eb0e Mon Sep 17 00:00:00 2001 From: NickSdot Date: Sun, 22 Jun 2025 23:05:37 +0700 Subject: [PATCH 10/13] fix: address review comment about "consumed too much" --- .../property_hooks/readonly_property_backed_inheritance_1.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Zend/tests/property_hooks/readonly_property_backed_inheritance_1.phpt b/Zend/tests/property_hooks/readonly_property_backed_inheritance_1.phpt index 915c93656bbbf..749ed9f772cb4 100644 --- a/Zend/tests/property_hooks/readonly_property_backed_inheritance_1.phpt +++ b/Zend/tests/property_hooks/readonly_property_backed_inheritance_1.phpt @@ -18,4 +18,4 @@ class Test extends ParentClass { ?> --EXPECTF-- -Fatal error: Cannot redeclare readonly property ParentClass::$prop as non-readonly Test::$prop %s on line %d +Fatal error: Cannot redeclare readonly property ParentClass::$prop as non-readonly Test::$prop in %s on line %d From 324e61e5ad4d226ea7b3e0226701bdcd709bc723 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Sun, 22 Jun 2025 23:29:24 +0700 Subject: [PATCH 11/13] refactor: tidied up test --- .../readonly_rfc_example_lazy_product.phpt | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt b/Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt index 4bc1c66806348..2b84a485868b3 100644 --- a/Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt +++ b/Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt @@ -70,9 +70,6 @@ echo $category2->name . "\n"; // same category instance returned var_dump($category1 === $category2); -// can't be wrong, huh? -var_dump($product); - // cannot set twice try { $categoryId->setValue($product, '420'); @@ -86,20 +83,4 @@ hit database Category 42 Category 42 bool(true) -object(LazyProduct)#2 (5) { - ["name"]=> - string(14) "Iced Chocolate" - ["price"]=> - float(1.99) - ["category"]=> - object(Category)#8 (1) { - ["name"]=> - string(11) "Category 42" - } - ["dbApi":"LazyProduct":private]=> - object(MockDbConnection)#6 (0) { - } - ["categoryId":"LazyProduct":private]=> - string(2) "42" -} Error: Cannot modify readonly property LazyProduct::$categoryId From 077c940a5a40c573a08a6885774a0257f0ea87cf Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 24 Jun 2025 12:03:41 +0700 Subject: [PATCH 12/13] refactor: addressed https://github.com/php/php-src/pull/18757#discussion_r2162464258 --- .../readonly_property_backed_in_abstract.phpt | 41 +++++++++++++++++++ ...readonly_property_virtual_in_abstract.phpt | 4 +- ..._property_virtual_invalid_in_abstract.phpt | 10 +++++ Zend/zend_compile.c | 28 ++++++------- 4 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 Zend/tests/property_hooks/readonly_property_backed_in_abstract.phpt create mode 100644 Zend/tests/property_hooks/readonly_property_virtual_invalid_in_abstract.phpt diff --git a/Zend/tests/property_hooks/readonly_property_backed_in_abstract.phpt b/Zend/tests/property_hooks/readonly_property_backed_in_abstract.phpt new file mode 100644 index 0000000000000..47cd0518c9c24 --- /dev/null +++ b/Zend/tests/property_hooks/readonly_property_backed_in_abstract.phpt @@ -0,0 +1,41 @@ +--TEST-- +Backed readonly property with hooks in abstract class +--FILE-- + $this->prop; + set => $value; + } + + public function __construct(int $v) { + $this->prop = $v; + } + + public function set(int $v) { + $this->prop = $v; + } +} + +class Child extends Test {} + +$ch = new Child(42); +var_dump($ch->prop); +try { + $ch->set(43); +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +try { + $ch->prop = 43; +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +var_dump($ch->prop); +?> +--EXPECT-- +int(42) +Error: Cannot modify readonly property Test::$prop +Error: Cannot modify protected(set) readonly property Test::$prop from global scope +int(42) diff --git a/Zend/tests/property_hooks/readonly_property_virtual_in_abstract.phpt b/Zend/tests/property_hooks/readonly_property_virtual_in_abstract.phpt index 7f6bdf4e09f3a..1816ebcec612a 100644 --- a/Zend/tests/property_hooks/readonly_property_virtual_in_abstract.phpt +++ b/Zend/tests/property_hooks/readonly_property_virtual_in_abstract.phpt @@ -1,5 +1,5 @@ --TEST-- -Hooked properties in abstract classes cannot be readonly +Virtual readonly property in abstract class triggers non-abstract body error --FILE-- --EXPECTF-- -Fatal error: Hooked properties in abstract classes cannot be declared readonly in %s on line %d +Fatal error: Non-abstract property hook must have a body in %s on line %d diff --git a/Zend/tests/property_hooks/readonly_property_virtual_invalid_in_abstract.phpt b/Zend/tests/property_hooks/readonly_property_virtual_invalid_in_abstract.phpt new file mode 100644 index 0000000000000..a6a4c9cdea724 --- /dev/null +++ b/Zend/tests/property_hooks/readonly_property_virtual_invalid_in_abstract.phpt @@ -0,0 +1,10 @@ +--TEST-- +Property hook cannot be both abstract and readonly +--FILE-- + +--EXPECTF-- +Fatal error: Abstract hooked properties cannot be declared readonly in %s on line %d \ No newline at end of file diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 73967e544a880..6330ddcda1624 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -8497,20 +8497,6 @@ static void zend_compile_property_hooks( { zend_class_entry *ce = CG(active_class_entry); - /* Allow hooks on backed readonly properties only. */ - if ((prop_info->flags & (ZEND_ACC_READONLY|ZEND_ACC_VIRTUAL)) == (ZEND_ACC_READONLY|ZEND_ACC_VIRTUAL)) { - - if (ce->ce_flags & ZEND_ACC_INTERFACE) { - zend_error_noreturn(E_COMPILE_ERROR, "Interface properties cannot be declared readonly"); - } - - if (ce->ce_flags & (ZEND_ACC_IMPLICIT_ABSTRACT_CLASS|ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)) { - zend_error_noreturn(E_COMPILE_ERROR, "Hooked properties in abstract classes cannot be declared readonly"); - } - - zend_error_noreturn(E_COMPILE_ERROR, "Hooked virtual properties cannot be declared readonly"); - } - if (hooks->children == 0) { zend_error_noreturn(E_COMPILE_ERROR, "Property hook list must not be empty"); } @@ -8666,6 +8652,20 @@ static void zend_compile_property_hooks( } } + /* Allow hooks on backed readonly properties only. */ + if ((prop_info->flags & (ZEND_ACC_READONLY|ZEND_ACC_VIRTUAL)) == (ZEND_ACC_READONLY|ZEND_ACC_VIRTUAL)) { + + if (ce->ce_flags & ZEND_ACC_INTERFACE) { + zend_error_noreturn(E_COMPILE_ERROR, "Interface properties cannot be declared readonly"); + } + + if (ce->ce_flags & (ZEND_ACC_IMPLICIT_ABSTRACT_CLASS|ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)) { + zend_error_noreturn(E_COMPILE_ERROR, "Abstract hooked properties cannot be declared readonly"); + } + + zend_error_noreturn(E_COMPILE_ERROR, "Hooked virtual properties cannot be declared readonly"); + } + ce->num_hooked_props++; /* See zend_link_hooked_object_iter(). */ From 3ec4f912302d2489678c6a977293e83a2b47794e Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 24 Jun 2025 12:19:48 +0700 Subject: [PATCH 13/13] refactor: addressed https://github.com/php/php-src/pull/18757#discussion_r2162443557 --- .../readonly_rfc_example_lazy_product.phpt | 45 +++---------------- 1 file changed, 7 insertions(+), 38 deletions(-) diff --git a/Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt b/Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt index 2b84a485868b3..961c7bcbd7e0c 100644 --- a/Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt +++ b/Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt @@ -3,39 +3,24 @@ Readonly classes can be constructed via reflection by ORM --FILE-- category ??= $this->dbApi->loadCategory($this->categoryId); + return $this->category ??= new Category($this->categoryId); } } } @@ -43,31 +28,16 @@ readonly class LazyProduct extends Product $reflect = new ReflectionClass(LazyProduct::class); $product = $reflect->newInstanceWithoutConstructor(); -$nameProperty = $reflect->getProperty('name'); -$nameProperty->setAccessible(true); -$nameProperty->setValue($product, 'Iced Chocolate'); - -$priceProperty = $reflect->getProperty('price'); -$priceProperty->setAccessible(true); -$priceProperty->setValue($product, 1.99); - -$db = $reflect->getProperty('dbApi'); -$db->setAccessible(true); -$db->setValue($product, new MockDbConnection()); - $categoryId = $reflect->getProperty('categoryId'); $categoryId->setAccessible(true); $categoryId->setValue($product, '42'); -// lazy loading, hit db $category1 = $product->category; -echo $category1->name . "\n"; - -// cached category returned $category2 = $product->category; -echo $category2->name . "\n"; -// same category instance returned +echo $category1->id . "\n"; +echo $category2->id . "\n"; + var_dump($category1 === $category2); // cannot set twice @@ -79,8 +49,7 @@ try { ?> --EXPECT-- -hit database -Category 42 -Category 42 +42 +42 bool(true) Error: Cannot modify readonly property LazyProduct::$categoryId