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) {