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..eab7079eec470 --- /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::class, ': ', $e->getMessage(), PHP_EOL; +} +try { + $t->prop = 43; +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +var_dump($t->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_class_property_backed_promoted.phpt b/Zend/tests/property_hooks/readonly_class_property_backed_promoted.phpt new file mode 100644 index 0000000000000..54a2529128571 --- /dev/null +++ b/Zend/tests/property_hooks/readonly_class_property_backed_promoted.phpt @@ -0,0 +1,39 @@ +--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::class, ': ', $e->getMessage(), PHP_EOL; +} +try { + $t->prop = 43; +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +var_dump($t->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_class_property_virtual_promoted.phpt b/Zend/tests/property_hooks/readonly_class_property_virtual_promoted.phpt new file mode 100644 index 0000000000000..e42a46747a2f9 --- /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 declared readonly in %s on line %d 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..9ea8a955a0892 --- /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::class, ': ', $e->getMessage(), PHP_EOL; +} +try { + $t->prop = 43; +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +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) +Error: Cannot modify readonly property Test::$prop +Error: 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" +} 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_backed_inheritance_1.phpt b/Zend/tests/property_hooks/readonly_property_backed_inheritance_1.phpt new file mode 100644 index 0000000000000..749ed9f772cb4 --- /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 in %s on line %d 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..2c45f88056331 --- /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 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..4959b202f669f --- /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::class, ': ', $e->getMessage(), PHP_EOL; +} + +try { + $t->prop = 43; // cannot write, visibility +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +?> +--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 +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 new file mode 100644 index 0000000000000..8ec3a3f2fc9e9 --- /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::class, ': ', $e->getMessage(), PHP_EOL; +} +try { + $t->prop = 43; +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +var_dump($t->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 new file mode 100644 index 0000000000000..1816ebcec612a --- /dev/null +++ b/Zend/tests/property_hooks/readonly_property_virtual_in_abstract.phpt @@ -0,0 +1,11 @@ +--TEST-- +Virtual readonly property in abstract class triggers non-abstract body error +--FILE-- + +--EXPECTF-- +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_in_class.phpt b/Zend/tests/property_hooks/readonly_property_virtual_in_class.phpt new file mode 100644 index 0000000000000..3f3cc88f360ec --- /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 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 new file mode 100644 index 0000000000000..63923750febb5 --- /dev/null +++ b/Zend/tests/property_hooks/readonly_property_virtual_in_interface.phpt @@ -0,0 +1,11 @@ +--TEST-- +Interface properties cannot be readonly +--FILE-- + +--EXPECTF-- +Fatal error: Interface properties cannot be declared readonly 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/tests/property_hooks/readonly_rfc_example_lazy_product.phpt b/Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt new file mode 100644 index 0000000000000..961c7bcbd7e0c --- /dev/null +++ b/Zend/tests/property_hooks/readonly_rfc_example_lazy_product.phpt @@ -0,0 +1,55 @@ +--TEST-- +Readonly classes can be constructed via reflection by ORM +--FILE-- +category ??= new Category($this->categoryId); + } + } +} + +$reflect = new ReflectionClass(LazyProduct::class); +$product = $reflect->newInstanceWithoutConstructor(); + +$categoryId = $reflect->getProperty('categoryId'); +$categoryId->setAccessible(true); +$categoryId->setValue($product, '42'); + +$category1 = $product->category; +$category2 = $product->category; + +echo $category1->id . "\n"; +echo $category2->id . "\n"; + +var_dump($category1 === $category2); + +// cannot set twice +try { + $categoryId->setValue($product, '420'); +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +42 +42 +bool(true) +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 new file mode 100644 index 0000000000000..32c1bf0d0cd04 --- /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::class, ': ', $e->getMessage(), PHP_EOL; +} + + +?> +--EXPECTF-- +object(PositivePoint)#1 (2) { + ["x"]=> + int(1) + ["y"]=> + int(1) +} +Error: Value must be greater 0 diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 0669d106f15e9..6330ddcda1624 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -8497,10 +8497,6 @@ 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"); - } - if (hooks->children == 0) { zend_error_noreturn(E_COMPILE_ERROR, "Property hook list must not be empty"); } @@ -8656,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(). */ 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