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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy