diff --git a/NEWS b/NEWS index 0322b9b452d..14845065252 100644 --- a/NEWS +++ b/NEWS @@ -16,6 +16,7 @@ PHP NEWS undefined). (Peter Kokot) . Fixed bug GH-15565 (--disable-ipv6 during compilation produces error EAI_SYSTEM not found). (nielsdos) + . Implemented asymmetric visibility for properties. (ilutov) - Date: . Fixed bug GH-13773 (DatePeriod not taking into account microseconds for end diff --git a/Zend/tests/asymmetric_visibility/__set.phpt b/Zend/tests/asymmetric_visibility/__set.phpt new file mode 100644 index 00000000000..7476f5304e2 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/__set.phpt @@ -0,0 +1,43 @@ +--TEST-- +Asymmetric visibility __set +--FILE-- +bar = $bar; + } + + public function unsetBar() { + unset($this->bar); + } + + public function __set(string $name, mixed $value) { + echo __CLASS__, '::', __METHOD__, "\n"; + } +} + +$foo = new Foo(); +try { + $foo->bar = 'baz'; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +$foo->setBar('baz'); +try { + $foo->bar = 'baz'; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +$foo->unsetBar(); +$foo->bar = 'baz'; + +?> +--EXPECT-- +Cannot modify private(set) property Foo::$bar from global scope +Cannot modify private(set) property Foo::$bar from global scope +Foo::Foo::__set diff --git a/Zend/tests/asymmetric_visibility/__unset.phpt b/Zend/tests/asymmetric_visibility/__unset.phpt new file mode 100644 index 00000000000..3d5977d7978 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/__unset.phpt @@ -0,0 +1,47 @@ +--TEST-- +Asymmetric visibility __unset +--FILE-- +bar = $bar; + } + + public function unsetBar() { + unset($this->bar); + } + + public function __unset($name) { + echo __METHOD__, "\n"; + } +} + +function test($foo) { + try { + unset($foo->bar); + } catch (Error $e) { + echo $e->getMessage(), "\n"; + } +} + +$foo = new Foo(); +test($foo); + +$foo->unsetBar(); +test($foo); + +$foo->setBar('bar'); +test($foo); + +$foo->unsetBar(); +test($foo); + +?> +--EXPECT-- +Cannot unset private(set) property Foo::$bar from global scope +Foo::__unset +Cannot unset private(set) property Foo::$bar from global scope +Foo::__unset diff --git a/Zend/tests/asymmetric_visibility/ast_printing.phpt b/Zend/tests/asymmetric_visibility/ast_printing.phpt new file mode 100644 index 00000000000..25803e67c09 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/ast_printing.phpt @@ -0,0 +1,28 @@ +--TEST-- +private(set) protected(set) ast printing +--INI-- +zend.assertions=1 +assert.exception=1 +--FILE-- +getMessage(); +} + +?> +--EXPECT-- +assert(function () { + class Foo { + public private(set) string $bar; + public protected(set) string $baz; + } + +} && false) diff --git a/Zend/tests/asymmetric_visibility/bug001.phpt b/Zend/tests/asymmetric_visibility/bug001.phpt new file mode 100644 index 00000000000..c306434d610 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/bug001.phpt @@ -0,0 +1,33 @@ +--TEST-- +Unset from __unset respects set visibility +--FILE-- +a); + } +} + +class D extends C { + public function __unset($name) { + unset($this->a); + } +} + +$c = new D(); +try { + unset($c->a); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +var_dump($c); + +?> +--EXPECTF-- +Cannot unset private(set) property C::$a from scope D +object(D)#%d (0) { + ["a"]=> + uninitialized(int) +} diff --git a/Zend/tests/asymmetric_visibility/bug002.phpt b/Zend/tests/asymmetric_visibility/bug002.phpt new file mode 100644 index 00000000000..1f6dc2b8527 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/bug002.phpt @@ -0,0 +1,33 @@ +--TEST-- +Set from __set respects set visibility +--FILE-- +a); + } +} + +class D extends C { + public function __set($name, $value) { + $this->a = $value; + } +} + +$c = new D(); +try { + $c->a = 2; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +var_dump($c); + +?> +--EXPECTF-- +Cannot modify private(set) property C::$a from scope D +object(D)#%d (0) { + ["a"]=> + uninitialized(int) +} diff --git a/Zend/tests/asymmetric_visibility/bug003.phpt b/Zend/tests/asymmetric_visibility/bug003.phpt new file mode 100644 index 00000000000..9f541f87f7c --- /dev/null +++ b/Zend/tests/asymmetric_visibility/bug003.phpt @@ -0,0 +1,28 @@ +--TEST-- +Explicitly unset property with a-vis still respects set visibility +--FILE-- +a); + } +} + +$c = new C(); +try { + $c->a = 2; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +try { + unset($c->a); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +Cannot modify private(set) property C::$a from global scope +Cannot unset private(set) property C::$a from global scope diff --git a/Zend/tests/asymmetric_visibility/bug004.phpt b/Zend/tests/asymmetric_visibility/bug004.phpt new file mode 100644 index 00000000000..f44d69cd216 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/bug004.phpt @@ -0,0 +1,18 @@ +--TEST-- +Missing property initialization for private(set) constructor promoted property +--FILE-- + +--EXPECTF-- +object(T)#%d (1) { + ["prop"]=> + string(4) "Test" +} diff --git a/Zend/tests/asymmetric_visibility/cpp_no_type.phpt b/Zend/tests/asymmetric_visibility/cpp_no_type.phpt new file mode 100644 index 00000000000..b1d169b6e18 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/cpp_no_type.phpt @@ -0,0 +1,14 @@ +--TEST-- +Asymmetric visibility in CPP with no type +--FILE-- + +--EXPECTF-- +Fatal error: Property with asymmetric visibility Foo::$bar must have type in %s on line %d diff --git a/Zend/tests/asymmetric_visibility/cpp_private.phpt b/Zend/tests/asymmetric_visibility/cpp_private.phpt new file mode 100644 index 00000000000..c308330d3ab --- /dev/null +++ b/Zend/tests/asymmetric_visibility/cpp_private.phpt @@ -0,0 +1,32 @@ +--TEST-- +Asymmetric visibility private(set) CPP +--FILE-- +bar = $bar; + } +} + +$foo = new Foo('bar'); +var_dump($foo->bar); + +try { + $foo->bar = 'baz'; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +$foo->setBar('baz'); +var_dump($foo->bar); + +?> +--EXPECT-- +string(3) "bar" +Cannot modify private(set) property Foo::$bar from global scope +string(3) "baz" diff --git a/Zend/tests/asymmetric_visibility/cpp_protected.phpt b/Zend/tests/asymmetric_visibility/cpp_protected.phpt new file mode 100644 index 00000000000..f13351e3200 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/cpp_protected.phpt @@ -0,0 +1,42 @@ +--TEST-- +Asymmetric visibility protected(set) CPP +--FILE-- +bar = $bar; + } +} + +class FooChild extends Foo { + public function setBarProtected($bar) { + $this->bar = $bar; + } +} + +$foo = new FooChild('bar'); +var_dump($foo->bar); + +try { + $foo->bar = 'baz'; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +$foo->setBarPrivate('baz'); +var_dump($foo->bar); + +$foo->setBarProtected('qux'); +var_dump($foo->bar); + +?> +--EXPECT-- +string(3) "bar" +Cannot modify protected(set) property Foo::$bar from global scope +string(3) "baz" +string(3) "qux" diff --git a/Zend/tests/asymmetric_visibility/cpp_wider_set_scope.phpt b/Zend/tests/asymmetric_visibility/cpp_wider_set_scope.phpt new file mode 100644 index 00000000000..45fcc965186 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/cpp_wider_set_scope.phpt @@ -0,0 +1,14 @@ +--TEST-- +Asymmetric visibility private(get) protected(set) in CPP is not allowed +--FILE-- + +--EXPECTF-- +Fatal error: Visibility of property Foo::$bar must not be weaker than set visibility in %s on line %d diff --git a/Zend/tests/asymmetric_visibility/decrease_scope_private_private.phpt b/Zend/tests/asymmetric_visibility/decrease_scope_private_private.phpt new file mode 100644 index 00000000000..affe965b45c --- /dev/null +++ b/Zend/tests/asymmetric_visibility/decrease_scope_private_private.phpt @@ -0,0 +1,13 @@ +--TEST-- +Asymmetric visibility private(get) private(set) is allowed +--FILE-- + +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/tests/asymmetric_visibility/decrease_scope_private_protected.phpt b/Zend/tests/asymmetric_visibility/decrease_scope_private_protected.phpt new file mode 100644 index 00000000000..625f12382cf --- /dev/null +++ b/Zend/tests/asymmetric_visibility/decrease_scope_private_protected.phpt @@ -0,0 +1,12 @@ +--TEST-- +Asymmetric visibility private(get) protected(set) not allowed +--FILE-- + +--EXPECTF-- +Fatal error: Visibility of property Foo::$bar must not be weaker than set visibility in %s on line %d diff --git a/Zend/tests/asymmetric_visibility/decrease_scope_protected_protected.phpt b/Zend/tests/asymmetric_visibility/decrease_scope_protected_protected.phpt new file mode 100644 index 00000000000..7d98b61b474 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/decrease_scope_protected_protected.phpt @@ -0,0 +1,13 @@ +--TEST-- +Asymmetric visibility protected(get) protected(set) is allowed +--FILE-- + +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/tests/asymmetric_visibility/dim_add.phpt b/Zend/tests/asymmetric_visibility/dim_add.phpt new file mode 100644 index 00000000000..f7bf0ceaf93 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/dim_add.phpt @@ -0,0 +1,34 @@ +--TEST-- +Asymmetric visibility DIM add +--FILE-- +bars[] = $bar; + } +} + +$foo = new Foo(); + +try { + $foo->bars[] = 'baz'; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +var_dump($foo->bars); + +$foo->addBar('baz'); +var_dump($foo->bars); + +?> +--EXPECT-- +Cannot indirectly modify private(set) property Foo::$bars from global scope +array(0) { +} +array(1) { + [0]=> + string(3) "baz" +} diff --git a/Zend/tests/asymmetric_visibility/duplicate_modifier.phpt b/Zend/tests/asymmetric_visibility/duplicate_modifier.phpt new file mode 100644 index 00000000000..d09eacd18de --- /dev/null +++ b/Zend/tests/asymmetric_visibility/duplicate_modifier.phpt @@ -0,0 +1,12 @@ +--TEST-- +Duplicate asymmetric visibility modifier +--FILE-- + +--EXPECTF-- +Fatal error: Multiple access type modifiers are not allowed in %s on line %d diff --git a/Zend/tests/asymmetric_visibility/duplicate_modifier_2.phpt b/Zend/tests/asymmetric_visibility/duplicate_modifier_2.phpt new file mode 100644 index 00000000000..832c57b296e --- /dev/null +++ b/Zend/tests/asymmetric_visibility/duplicate_modifier_2.phpt @@ -0,0 +1,12 @@ +--TEST-- +Duplicate asymmetric visibility modifier +--FILE-- + +--EXPECTF-- +Fatal error: Multiple access type modifiers are not allowed in %s on line %d diff --git a/Zend/tests/asymmetric_visibility/nested_write.phpt b/Zend/tests/asymmetric_visibility/nested_write.phpt new file mode 100644 index 00000000000..8386b6b2503 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/nested_write.phpt @@ -0,0 +1,24 @@ +--TEST-- +Asymmetric visibility nested write +--FILE-- +bar = new Bar; + } +} + +class Bar { + public int $baz; +} + +$foo = new Foo(); +$foo->bar->baz = 42; +var_dump($foo->bar->baz); + +?> +--EXPECT-- +int(42) diff --git a/Zend/tests/asymmetric_visibility/no_type.phpt b/Zend/tests/asymmetric_visibility/no_type.phpt new file mode 100644 index 00000000000..3e48bdbb06f --- /dev/null +++ b/Zend/tests/asymmetric_visibility/no_type.phpt @@ -0,0 +1,12 @@ +--TEST-- +Asymmetric visibility with no type +--FILE-- + +--EXPECTF-- +Fatal error: Property with asymmetric visibility Foo::$bar must have type in %s on line %d diff --git a/Zend/tests/asymmetric_visibility/object_reference.phpt b/Zend/tests/asymmetric_visibility/object_reference.phpt new file mode 100644 index 00000000000..26cc0838610 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/object_reference.phpt @@ -0,0 +1,37 @@ +--TEST-- +Attempting to obtain reference of object of private(set) object returns a copy instead +--FILE-- +bar = new Bar(); + } +} + +class Bar {} + +function test() { + $foo = new Foo(); + $bar = &$foo->bar; + var_dump($foo); +} + +test(); +// Test zend_fetch_property_address with warmed cache +test(); + +?> +--EXPECT-- +object(Foo)#1 (1) { + ["bar"]=> + object(Bar)#2 (0) { + } +} +object(Foo)#2 (1) { + ["bar"]=> + object(Bar)#1 (0) { + } +} diff --git a/Zend/tests/asymmetric_visibility/override_private_public.phpt b/Zend/tests/asymmetric_visibility/override_private_public.phpt new file mode 100644 index 00000000000..122c6efbda6 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/override_private_public.phpt @@ -0,0 +1,16 @@ +--TEST-- +private(set) property is implicitly final +--FILE-- + +--EXPECTF-- +Fatal error: Cannot override final property A::$foo in %s on line %d diff --git a/Zend/tests/asymmetric_visibility/override_protected_private.phpt b/Zend/tests/asymmetric_visibility/override_protected_private.phpt new file mode 100644 index 00000000000..958e886dd80 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/override_protected_private.phpt @@ -0,0 +1,16 @@ +--TEST-- +Overwritten protected asymmetric property with private asymmetric property +--FILE-- + +--EXPECTF-- +Fatal error: Set access level of B::$foo must be protected(set) (as in class A) or weaker in %s on line %d diff --git a/Zend/tests/asymmetric_visibility/override_protected_public.phpt b/Zend/tests/asymmetric_visibility/override_protected_public.phpt new file mode 100644 index 00000000000..36890c50b4b --- /dev/null +++ b/Zend/tests/asymmetric_visibility/override_protected_public.phpt @@ -0,0 +1,20 @@ +--TEST-- +Overwritten protected asymmetric property with public property +--FILE-- +foo = 'foo'; +echo $b->foo, "\n"; + +?> +--EXPECT-- +foo diff --git a/Zend/tests/asymmetric_visibility/override_public_private.phpt b/Zend/tests/asymmetric_visibility/override_public_private.phpt new file mode 100644 index 00000000000..f1b2191ac16 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/override_public_private.phpt @@ -0,0 +1,16 @@ +--TEST-- +Overwritten public property with private asymmetric property +--FILE-- + +--EXPECTF-- +Fatal error: Set access level of B::$foo must be omitted (as in class A) in %s on line %d diff --git a/Zend/tests/asymmetric_visibility/override_public_protected.phpt b/Zend/tests/asymmetric_visibility/override_public_protected.phpt new file mode 100644 index 00000000000..5611d368b19 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/override_public_protected.phpt @@ -0,0 +1,16 @@ +--TEST-- +Overwritten public property with protected asymmetric property +--FILE-- + +--EXPECTF-- +Fatal error: Set access level of B::$foo must be omitted (as in class A) in %s on line %d diff --git a/Zend/tests/asymmetric_visibility/private.phpt b/Zend/tests/asymmetric_visibility/private.phpt new file mode 100644 index 00000000000..48e557c6e87 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/private.phpt @@ -0,0 +1,61 @@ +--TEST-- +Asymmetric visibility private(set) +--FILE-- +bar = $bar; + } + + public function setBaz($baz) { + $this->baz = $baz; + } +} + +class FooChild extends Foo { + public function modifyBar($bar) { + $this->bar = $bar; + } +} + +$foo = new Foo(); +var_dump($foo->bar); + +try { + $foo->bar = 'baz'; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +$foo->setBar('baz'); +var_dump($foo->bar); + +try { + $foo->baz = 'baz2'; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +$foo->setBaz('baz2'); +var_dump($foo->baz); + +$child = new FooChild(); +try { + $child->modifyBar('baz'); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +string(3) "bar" +Cannot modify private(set) property Foo::$bar from global scope +string(3) "baz" +Cannot modify private(set) property Foo::$baz from global scope +string(4) "baz2" +Cannot modify private(set) property Foo::$bar from scope FooChild diff --git a/Zend/tests/asymmetric_visibility/protected.phpt b/Zend/tests/asymmetric_visibility/protected.phpt new file mode 100644 index 00000000000..49a39b39936 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/protected.phpt @@ -0,0 +1,66 @@ +--TEST-- +Asymmetric visibility protected(set) +--FILE-- +bar = $bar; + } + + public function setBazPrivate($baz) { + $this->baz = $baz; + } +} + +class FooChild extends Foo { + public function setBarProtected($bar) { + $this->bar = $bar; + } + + public function setBazProtected($baz) { + $this->baz = $baz; + } +} + +$foo = new FooChild(); +var_dump($foo->bar); + +try { + $foo->bar = 'baz'; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +$foo->setBarPrivate('baz'); +var_dump($foo->bar); + +$foo->setBarProtected('qux'); +var_dump($foo->bar); + +try { + $foo->baz = 'baz'; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +$foo->setbazPrivate('baz2'); +var_dump($foo->baz); + +$foo->setbazProtected('baz3'); +var_dump($foo->baz); + + +?> +--EXPECT-- +string(3) "bar" +Cannot modify protected(set) property Foo::$bar from global scope +string(3) "baz" +string(3) "qux" +Cannot modify protected(set) property Foo::$baz from global scope +string(4) "baz2" +string(4) "baz3" diff --git a/Zend/tests/asymmetric_visibility/readonly.phpt b/Zend/tests/asymmetric_visibility/readonly.phpt new file mode 100644 index 00000000000..8fa77aafb1d --- /dev/null +++ b/Zend/tests/asymmetric_visibility/readonly.phpt @@ -0,0 +1,62 @@ +--TEST-- +Asymmetric visibility with readonly +--FILE-- +pDefault = 1; + $this->pPrivate = 1; + $this->pProtected = 1; + $this->pPublic = 1; + } +} + +class C extends P { + public function __construct() { + $this->pDefault = 1; + try { + $this->pPrivate = 1; + } catch (Error $e) { + echo $e->getMessage(), "\n"; + } + $this->pProtected = 1; + $this->pPublic = 1; + } +} + +function test() { + $p = new ReflectionClass(P::class)->newInstanceWithoutConstructor(); + try { + $p->pDefault = 1; + } catch (Error $e) { + echo $e->getMessage(), "\n"; + } + try { + $p->pPrivate = 1; + } catch (Error $e) { + echo $e->getMessage(), "\n"; + } + try { + $p->pProtected = 1; + } catch (Error $e) { + echo $e->getMessage(), "\n"; + } + $p->pPublic = 1; +} + +new P(); +new C(); +test(); + +?> +--EXPECT-- +Cannot modify private(set) property P::$pPrivate from scope C +Cannot modify protected(set) property P::$pDefault from global scope +Cannot modify private(set) property P::$pPrivate from global scope +Cannot modify protected(set) property P::$pProtected from global scope diff --git a/Zend/tests/asymmetric_visibility/reference.phpt b/Zend/tests/asymmetric_visibility/reference.phpt new file mode 100644 index 00000000000..ae527141828 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/reference.phpt @@ -0,0 +1,32 @@ +--TEST-- +Asymmetric visibility reference +--FILE-- +bar; + $bar++; + } +} + +$foo = new Foo(); + +try { + $bar = &$foo->bar; + $bar++; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +var_dump($foo->bar); + +$foo->test(); +var_dump($foo->bar); + +?> +--EXPECT-- +Cannot indirectly modify private(set) property Foo::$bar from global scope +int(0) +int(1) diff --git a/Zend/tests/asymmetric_visibility/reference_2.phpt b/Zend/tests/asymmetric_visibility/reference_2.phpt new file mode 100644 index 00000000000..d4ef6c03a78 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/reference_2.phpt @@ -0,0 +1,30 @@ +--TEST-- +Asymmetric visibility reference in forbidden scope +--FILE-- +prop; + $prop = 42; +} + +try { + test(new C()); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +try { + test(new C()); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +Cannot indirectly modify private(set) property C::$prop from global scope +Cannot indirectly modify private(set) property C::$prop from global scope diff --git a/Zend/tests/asymmetric_visibility/scope_rebinding.phpt b/Zend/tests/asymmetric_visibility/scope_rebinding.phpt new file mode 100644 index 00000000000..8c10d10f77a --- /dev/null +++ b/Zend/tests/asymmetric_visibility/scope_rebinding.phpt @@ -0,0 +1,36 @@ +--TEST-- +Changing scope with Closure::bindTo() does not confuse asymmetric visibility +--FILE-- +bar++; +}; + +($c->bindTo(null, Foo::class))(); +var_dump($foo->bar); +try { + $c(); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +try { + ($c->bindTo(null, Bar::class))(); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +var_dump($foo->bar); + +?> +--EXPECT-- +int(2) +Cannot modify private(set) property Foo::$bar from global scope +Cannot modify private(set) property Foo::$bar from scope Bar +int(2) diff --git a/Zend/tests/asymmetric_visibility/unset.phpt b/Zend/tests/asymmetric_visibility/unset.phpt new file mode 100644 index 00000000000..3fd3d767308 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/unset.phpt @@ -0,0 +1,67 @@ +--TEST-- +Asymmetric visibility unset protected(set) +--FILE-- +bar = $bar; + } + + public function setSecret($s) { + $this->secret = $s; + } + + public function unsetBarPrivate() { + unset($this->bar); + } +} + +class FooChild extends Foo { + public function unsetBarProtected() { + unset($this->bar); + } + + public function unsetSecret() { + unset($this->secret); + } +} + +$foo = new FooChild(); + +$foo->setBar('bar'); +try { + unset($foo->bar); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +var_dump($foo->bar ?? 'unset'); + +$foo->setBar('bar'); +$foo->unsetBarPrivate(); +var_dump($foo->bar ?? 'unset'); + +$foo->setBar('bar'); +$foo->unsetBarProtected(); +var_dump($foo->bar ?? 'unset'); + +$foo->setSecret('beep'); +try { + $foo->unsetSecret(); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +var_dump($foo->secret ?? 'unset'); + +?> +--EXPECT-- +Cannot unset protected(set) property Foo::$bar from global scope +string(3) "bar" +string(5) "unset" +string(5) "unset" +Cannot unset private(set) property Foo::$secret from scope FooChild +string(4) "beep" diff --git a/Zend/tests/asymmetric_visibility/unshared_rw_cache_slot.phpt b/Zend/tests/asymmetric_visibility/unshared_rw_cache_slot.phpt new file mode 100644 index 00000000000..d81f4d3891b --- /dev/null +++ b/Zend/tests/asymmetric_visibility/unshared_rw_cache_slot.phpt @@ -0,0 +1,37 @@ +--TEST-- +R/w cache slots should be unshared +--FILE-- +bar); + $this->bar = $bar; + } +} + +$c = new C(); +try { + $c->setBar(1); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +try { + $c->setBar(1); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +var_dump($c); + +?> +--EXPECTF-- +Typed property P::$bar must not be accessed before initialization +Typed property P::$bar must not be accessed before initialization +object(C)#%d (0) { + ["bar"]=> + uninitialized(string) +} diff --git a/Zend/tests/asymmetric_visibility/variation.phpt b/Zend/tests/asymmetric_visibility/variation.phpt new file mode 100644 index 00000000000..03cac375f71 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/variation.phpt @@ -0,0 +1,134 @@ +--TEST-- +Asymmetric visibility variations +--FILE-- +prop = 1; + $this->array = []; + } + + public function r() { + echo $this->prop; + } + + public function w() { + $this->prop = 1; + echo 'done'; + } + + public function rw() { + $this->prop += 1; + echo 'done'; + } + + public function im() { + $this->array[] = 1; + echo 'done'; + } + + public function is() { + echo (int) isset($this->prop); + } + + public function us() { + unset($this->prop); + echo 'done'; + } + + public function us_dim() { + unset($this->array[0]); + echo 'done'; + } +} + +function r($test) { + echo $test->prop; +} + +function w($test) { + $test->prop = 0; + echo 'done'; +} + +function rw($test) { + $test->prop += 1; + echo 'done'; +} + +function im($test) { + $test->array[] = 1; + echo 'done'; +} + +function is($test) { + echo (int) isset($test->prop); +} + +function us($test) { + unset($test->prop); + echo 'done'; +} + +function us_dim($test) { + unset($test->array[0]); + echo 'done'; +} + +foreach ([true, false] as $init) { + foreach ([true, false] as $scope) { + foreach (['r', 'w', 'rw', 'im', 'is', 'us', 'us_dim'] as $op) { + $test = new Test(); + if ($init) { + $test->init(); + } + + echo 'Init: ' . ((int) $init) . ', scope: ' . ((int) $scope) . ', op: ' . $op . ": "; + try { + if ($scope) { + $test->{$op}(); + } else { + $op($test); + } + } catch (Error $e) { + echo $e->getMessage(); + } + echo "\n"; + } + } +} + +?> +--EXPECT-- +Init: 1, scope: 1, op: r: 1 +Init: 1, scope: 1, op: w: done +Init: 1, scope: 1, op: rw: done +Init: 1, scope: 1, op: im: done +Init: 1, scope: 1, op: is: 1 +Init: 1, scope: 1, op: us: done +Init: 1, scope: 1, op: us_dim: done +Init: 1, scope: 0, op: r: 1 +Init: 1, scope: 0, op: w: Cannot modify private(set) property Test::$prop from global scope +Init: 1, scope: 0, op: rw: Cannot modify private(set) property Test::$prop from global scope +Init: 1, scope: 0, op: im: Cannot indirectly modify private(set) property Test::$array from global scope +Init: 1, scope: 0, op: is: 1 +Init: 1, scope: 0, op: us: Cannot unset private(set) property Test::$prop from global scope +Init: 1, scope: 0, op: us_dim: Cannot indirectly modify private(set) property Test::$array from global scope +Init: 0, scope: 1, op: r: Typed property Test::$prop must not be accessed before initialization +Init: 0, scope: 1, op: w: done +Init: 0, scope: 1, op: rw: Typed property Test::$prop must not be accessed before initialization +Init: 0, scope: 1, op: im: done +Init: 0, scope: 1, op: is: 0 +Init: 0, scope: 1, op: us: done +Init: 0, scope: 1, op: us_dim: done +Init: 0, scope: 0, op: r: Typed property Test::$prop must not be accessed before initialization +Init: 0, scope: 0, op: w: Cannot modify private(set) property Test::$prop from global scope +Init: 0, scope: 0, op: rw: Typed property Test::$prop must not be accessed before initialization +Init: 0, scope: 0, op: im: Cannot indirectly modify private(set) property Test::$array from global scope +Init: 0, scope: 0, op: is: 0 +Init: 0, scope: 0, op: us: Cannot unset private(set) property Test::$prop from global scope +Init: 0, scope: 0, op: us_dim: done diff --git a/Zend/tests/asymmetric_visibility/variation_nested.phpt b/Zend/tests/asymmetric_visibility/variation_nested.phpt new file mode 100644 index 00000000000..c1dc753a944 --- /dev/null +++ b/Zend/tests/asymmetric_visibility/variation_nested.phpt @@ -0,0 +1,84 @@ +--TEST-- +Asymmetric visibility nested variations +--FILE-- +prop = new Inner(); + } +} + +function r($test) { + echo $test->prop->prop; +} + +function w($test) { + $test->prop->prop = 0; + echo 'done'; +} + +function rw($test) { + $test->prop->prop += 1; + echo 'done'; +} + +function im($test) { + $test->prop->array[] = 1; + echo 'done'; +} + +function is($test) { + echo (int) isset($test->prop->prop); +} + +function us($test) { + unset($test->prop->prop); + echo 'done'; +} + +function us_dim($test) { + unset($test->prop->array[0]); + echo 'done'; +} + +foreach ([true, false] as $init) { + foreach (['r', 'w', 'rw', 'im', 'is', 'us', 'us_dim'] as $op) { + $test = new Test(); + if ($init) { + $test->init(); + } + + echo 'Init: ' . ((int) $init) . ', op: ' . $op . ": "; + try { + $op($test); + } catch (Error $e) { + echo $e->getMessage(); + } + echo "\n"; + } +} + +?> +--EXPECT-- +Init: 1, op: r: 1 +Init: 1, op: w: done +Init: 1, op: rw: done +Init: 1, op: im: done +Init: 1, op: is: 1 +Init: 1, op: us: done +Init: 1, op: us_dim: done +Init: 0, op: r: Typed property Test::$prop must not be accessed before initialization +Init: 0, op: w: Cannot indirectly modify private(set) property Test::$prop from global scope +Init: 0, op: rw: Typed property Test::$prop must not be accessed before initialization +Init: 0, op: im: Cannot indirectly modify private(set) property Test::$prop from global scope +Init: 0, op: is: 0 +Init: 0, op: us: done +Init: 0, op: us_dim: done diff --git a/Zend/tests/property_hooks/abstract_get_set_readonly.phpt b/Zend/tests/property_hooks/abstract_get_set_readonly.phpt new file mode 100644 index 00000000000..644ffb47496 --- /dev/null +++ b/Zend/tests/property_hooks/abstract_get_set_readonly.phpt @@ -0,0 +1,13 @@ +--TEST-- +readonly property does not satisfy get/set abstract property +--FILE-- + +--EXPECTF-- +Fatal error: Class C contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (P::$prop::set) in %s on line %d diff --git a/Zend/tests/property_hooks/interface_get_set_readonly.phpt b/Zend/tests/property_hooks/interface_get_set_readonly.phpt index 685eb4ee8bd..692a0bc7e78 100644 --- a/Zend/tests/property_hooks/interface_get_set_readonly.phpt +++ b/Zend/tests/property_hooks/interface_get_set_readonly.phpt @@ -1,5 +1,7 @@ --TEST-- readonly property does not satisfy get/set interface property +--DESCRIPTION-- +The error message should be improved, the set access level comes from readonly. --FILE-- --EXPECTF-- -Fatal error: Class C contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (I::$prop::set) in %s on line %d +Fatal error: Set access level of C::$prop must be omitted (as in class I) in %s on line %d diff --git a/Zend/tests/readonly_props/initialization_scope.phpt b/Zend/tests/readonly_props/initialization_scope.phpt index b01dacf2d42..f3e87efb2df 100644 --- a/Zend/tests/readonly_props/initialization_scope.phpt +++ b/Zend/tests/readonly_props/initialization_scope.phpt @@ -22,12 +22,10 @@ try { } catch (Error $e) { echo $e->getMessage(), "\n"; } -try { - $test->initProtected(); -} catch (Error $e) { - echo $e->getMessage(), "\n"; -} +$test->initProtected(); +var_dump($test); +$test = new B; $test->initPrivate(); var_dump($test->prop); @@ -56,17 +54,20 @@ class Y extends X { } $test = new Y; -try { - $test->initFromParent(); -} catch (Error $e) { - echo $e->getMessage(), "\n"; -} +$test->initFromParent(); +var_dump($test); ?> ---EXPECT-- -Cannot initialize readonly property A::$prop from global scope -Cannot initialize readonly property A::$prop from scope B +--EXPECTF-- +Cannot modify protected(set) property A::$prop from global scope +object(B)#%d (1) { + ["prop"]=> + int(2) +} int(3) int(1) int(3) -Cannot initialize readonly property Y::$prop from scope X +object(Y)#%d (1) { + ["prop"]=> + int(1) +} diff --git a/Zend/tests/readonly_props/magic_get_set.phpt b/Zend/tests/readonly_props/magic_get_set.phpt index 201c4e3bfe2..2ce24cf2f11 100644 --- a/Zend/tests/readonly_props/magic_get_set.phpt +++ b/Zend/tests/readonly_props/magic_get_set.phpt @@ -64,8 +64,8 @@ try { --EXPECT-- bool(false) Typed property Test::$prop must not be accessed before initialization -Cannot initialize readonly property Test::$prop from global scope -Cannot unset readonly property Test::$prop from global scope +Cannot modify protected(set) property Test::$prop from global scope +Cannot unset protected(set) property Test::$prop from global scope Test::__isset(prop) bool(true) Test::__get(prop) diff --git a/Zend/tests/readonly_props/public_set_non_readonly.phpt b/Zend/tests/readonly_props/public_set_non_readonly.phpt new file mode 100644 index 00000000000..05bb1c0db32 --- /dev/null +++ b/Zend/tests/readonly_props/public_set_non_readonly.phpt @@ -0,0 +1,13 @@ +--TEST-- +public(set) is allowed on non-readonly +--FILE-- + +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/tests/readonly_props/unset.phpt b/Zend/tests/readonly_props/unset.phpt index 5bf2107a09b..d6c3f1edf54 100644 --- a/Zend/tests/readonly_props/unset.phpt +++ b/Zend/tests/readonly_props/unset.phpt @@ -61,4 +61,4 @@ Test2::__get int(1) int(1) Cannot unset readonly property Test2::$prop -Cannot unset readonly property Test3::$prop from global scope +Cannot unset protected(set) property Test3::$prop from global scope diff --git a/Zend/tests/readonly_props/variation.phpt b/Zend/tests/readonly_props/variation.phpt index 1c93f3f6f59..12d4b41ed9f 100644 --- a/Zend/tests/readonly_props/variation.phpt +++ b/Zend/tests/readonly_props/variation.phpt @@ -5,9 +5,11 @@ Readonly variations class Test { public readonly int $prop; + public readonly array $array; public function init() { $this->prop = 1; + $this->array = []; } public function r() { @@ -37,6 +39,11 @@ class Test { unset($this->prop); echo 'done'; } + + public function us_dim() { + unset($this->array[0]); + echo 'done'; + } } function r($test) { @@ -67,9 +74,14 @@ function us($test) { echo 'done'; } +function us_dim($test) { + unset($test->array[0]); + echo 'done'; +} + foreach ([true, false] as $init) { foreach ([true, false] as $scope) { - foreach (['r', 'w', 'rw', 'im', 'is', 'us'] as $op) { + foreach (['r', 'w', 'rw', 'im', 'is', 'us', 'us_dim'] as $op) { $test = new Test(); if ($init) { $test->init(); @@ -98,21 +110,25 @@ Init: 1, scope: 1, op: rw: Cannot modify readonly property Test::$prop Init: 1, scope: 1, op: im: Cannot indirectly modify readonly property Test::$prop Init: 1, scope: 1, op: is: 1 Init: 1, scope: 1, op: us: Cannot unset readonly property Test::$prop +Init: 1, scope: 1, op: us_dim: Cannot indirectly modify readonly property Test::$array Init: 1, scope: 0, op: r: 1 Init: 1, scope: 0, op: w: Cannot modify readonly property Test::$prop Init: 1, scope: 0, op: rw: Cannot modify readonly property Test::$prop Init: 1, scope: 0, op: im: Cannot indirectly modify readonly property Test::$prop Init: 1, scope: 0, op: is: 1 Init: 1, scope: 0, op: us: Cannot unset readonly property Test::$prop +Init: 1, scope: 0, op: us_dim: Cannot indirectly modify readonly property Test::$array Init: 0, scope: 1, op: r: Typed property Test::$prop must not be accessed before initialization Init: 0, scope: 1, op: w: done Init: 0, scope: 1, op: rw: Typed property Test::$prop must not be accessed before initialization Init: 0, scope: 1, op: im: Cannot indirectly modify readonly property Test::$prop Init: 0, scope: 1, op: is: 0 Init: 0, scope: 1, op: us: done +Init: 0, scope: 1, op: us_dim: done Init: 0, scope: 0, op: r: Typed property Test::$prop must not be accessed before initialization -Init: 0, scope: 0, op: w: Cannot initialize readonly property Test::$prop from global scope +Init: 0, scope: 0, op: w: Cannot modify protected(set) property Test::$prop from global scope Init: 0, scope: 0, op: rw: Typed property Test::$prop must not be accessed before initialization Init: 0, scope: 0, op: im: Cannot indirectly modify readonly property Test::$prop Init: 0, scope: 0, op: is: 0 -Init: 0, scope: 0, op: us: Cannot unset readonly property Test::$prop from global scope +Init: 0, scope: 0, op: us: Cannot unset protected(set) property Test::$prop from global scope +Init: 0, scope: 0, op: us_dim: done diff --git a/Zend/tests/readonly_props/variation_nested.phpt b/Zend/tests/readonly_props/variation_nested.phpt index 72a7925da99..31608afcc2c 100644 --- a/Zend/tests/readonly_props/variation_nested.phpt +++ b/Zend/tests/readonly_props/variation_nested.phpt @@ -44,8 +44,13 @@ function us($test) { echo 'done'; } +function us_dim($test) { + unset($test->prop->array[0]); + echo 'done'; +} + foreach ([true, false] as $init) { - foreach (['r', 'w', 'rw', 'im', 'is', 'us'] as $op) { + foreach (['r', 'w', 'rw', 'im', 'is', 'us', 'us_dim'] as $op) { $test = new Test(); if ($init) { $test->init(); @@ -69,9 +74,11 @@ Init: 1, op: rw: done Init: 1, op: im: done Init: 1, op: is: 1 Init: 1, op: us: done +Init: 1, op: us_dim: done Init: 0, op: r: Typed property Test::$prop must not be accessed before initialization Init: 0, op: w: Cannot indirectly modify readonly property Test::$prop Init: 0, op: rw: Typed property Test::$prop must not be accessed before initialization Init: 0, op: im: Cannot indirectly modify readonly property Test::$prop Init: 0, op: is: 0 Init: 0, op: us: done +Init: 0, op: us_dim: done diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 27a914aab9e..1d10f61e621 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -4523,6 +4523,33 @@ ZEND_API zend_property_info *zend_declare_typed_property(zend_class_entry *ce, z if (!(access_type & ZEND_ACC_PPP_MASK)) { access_type |= ZEND_ACC_PUBLIC; } + /* Add the protected(set) bit for public readonly properties with no set visibility. */ + if ((access_type & (ZEND_ACC_PUBLIC|ZEND_ACC_READONLY|ZEND_ACC_PPP_SET_MASK)) == (ZEND_ACC_PUBLIC|ZEND_ACC_READONLY)) { + access_type |= ZEND_ACC_PROTECTED_SET; + } else if (UNEXPECTED(access_type & ZEND_ACC_PPP_SET_MASK)) { + if (!ZEND_TYPE_IS_SET(type)) { + zend_error_noreturn(ce->type == ZEND_INTERNAL_CLASS ? E_CORE_ERROR : E_COMPILE_ERROR, + "Property with asymmetric visibility %s::$%s must have type", + ZSTR_VAL(ce->name), ZSTR_VAL(name)); + } + uint32_t get_visibility = zend_visibility_to_set_visibility(access_type & ZEND_ACC_PPP_MASK); + uint32_t set_visibility = access_type & ZEND_ACC_PPP_SET_MASK; + if (get_visibility > set_visibility) { + zend_error_noreturn(ce->type == ZEND_INTERNAL_CLASS ? E_CORE_ERROR : E_COMPILE_ERROR, + "Visibility of property %s::$%s must not be weaker than set visibility", + ZSTR_VAL(ce->name), ZSTR_VAL(name)); + } + /* Remove equivalent set visibility. */ + if (((access_type & (ZEND_ACC_PUBLIC|ZEND_ACC_PUBLIC_SET)) == (ZEND_ACC_PUBLIC|ZEND_ACC_PUBLIC_SET)) + || ((access_type & (ZEND_ACC_PROTECTED|ZEND_ACC_PROTECTED_SET)) == (ZEND_ACC_PROTECTED|ZEND_ACC_PROTECTED_SET)) + || ((access_type & (ZEND_ACC_PRIVATE|ZEND_ACC_PRIVATE_SET)) == (ZEND_ACC_PRIVATE|ZEND_ACC_PRIVATE_SET))) { + access_type &= ~ZEND_ACC_PPP_SET_MASK; + } + /* private(set) properties are implicitly final. */ + if (access_type & ZEND_ACC_PRIVATE_SET) { + access_type |= ZEND_ACC_FINAL; + } + } /* Virtual properties have no backing storage, the offset should never be used. However, the * virtual flag cannot be definitively determined at compile time. Allow using default values diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 5ecda768704..d33747412ef 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -1697,7 +1697,7 @@ static ZEND_COLD void zend_ast_export_attributes(smart_str *str, zend_ast *ast, } } -static ZEND_COLD void zend_ast_export_visibility(smart_str *str, uint32_t flags) { +static ZEND_COLD void zend_ast_export_visibility(smart_str *str, uint32_t flags, zend_modifier_target target) { if (flags & ZEND_ACC_PUBLIC) { smart_str_appends(str, "public "); } else if (flags & ZEND_ACC_PROTECTED) { @@ -1705,6 +1705,16 @@ static ZEND_COLD void zend_ast_export_visibility(smart_str *str, uint32_t flags) } else if (flags & ZEND_ACC_PRIVATE) { smart_str_appends(str, "private "); } + + if (target == ZEND_MODIFIER_TARGET_PROPERTY || target == ZEND_MODIFIER_TARGET_CPP) { + if (flags & ZEND_ACC_PRIVATE_SET) { + smart_str_appends(str, "private(set) "); + } else if (flags & ZEND_ACC_PROTECTED_SET) { + smart_str_appends(str, "protected(set) "); + } else if (flags & ZEND_ACC_PUBLIC_SET) { + smart_str_appends(str, "public(set) "); + } + } } static ZEND_COLD void zend_ast_export_type(smart_str *str, zend_ast *ast, int indent) { @@ -1815,7 +1825,7 @@ tail_call: zend_ast_export_attributes(str, decl->child[4], indent, newlines); } - zend_ast_export_visibility(str, decl->flags); + zend_ast_export_visibility(str, decl->flags, ZEND_MODIFIER_TARGET_METHOD); if (decl->flags & ZEND_ACC_STATIC) { smart_str_appends(str, "static "); @@ -1941,7 +1951,7 @@ simple_list: zend_ast_export_attributes(str, ast->child[2], indent, 1); } - zend_ast_export_visibility(str, ast->attr); + zend_ast_export_visibility(str, ast->attr, ZEND_MODIFIER_TARGET_PROPERTY); if (ast->attr & ZEND_ACC_STATIC) { smart_str_appends(str, "static "); @@ -1967,7 +1977,7 @@ simple_list: zend_ast_export_attributes(str, ast->child[1], indent, 1); } - zend_ast_export_visibility(str, ast->attr); + zend_ast_export_visibility(str, ast->attr, ZEND_MODIFIER_TARGET_CONSTANT); smart_str_appends(str, "const "); if (ast->child[2]) { zend_ast_export_type(str, ast->child[2], indent); @@ -2388,7 +2398,7 @@ simple_list: for (uint32_t i = 0; i < hook_list->children; i++) { zend_ast_decl *hook = (zend_ast_decl *)hook_list->child[i]; - zend_ast_export_visibility(str, hook->flags); + zend_ast_export_visibility(str, hook->flags, ZEND_MODIFIER_TARGET_PROPERTY); if (hook->flags & ZEND_ACC_FINAL) { smart_str_appends(str, "final "); } diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 003e2bd8312..36231a5610f 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -860,6 +860,12 @@ static char *zend_modifier_token_to_string(uint32_t token) return "readonly"; case T_ABSTRACT: return "abstract"; + case T_PUBLIC_SET: + return "public(set)"; + case T_PROTECTED_SET: + return "protected(set)"; + case T_PRIVATE_SET: + return "private(set)"; EMPTY_SWITCH_DEFAULT_CASE() } } @@ -905,6 +911,21 @@ uint32_t zend_modifier_token_to_flag(zend_modifier_target target, uint32_t token return ZEND_ACC_STATIC; } break; + case T_PUBLIC_SET: + if (target == ZEND_MODIFIER_TARGET_PROPERTY || target == ZEND_MODIFIER_TARGET_CPP) { + return ZEND_ACC_PUBLIC_SET; + } + break; + case T_PROTECTED_SET: + if (target == ZEND_MODIFIER_TARGET_PROPERTY || target == ZEND_MODIFIER_TARGET_CPP) { + return ZEND_ACC_PROTECTED_SET; + } + break; + case T_PRIVATE_SET: + if (target == ZEND_MODIFIER_TARGET_PROPERTY || target == ZEND_MODIFIER_TARGET_CPP) { + return ZEND_ACC_PRIVATE_SET; + } + break; } char *member; @@ -1020,6 +1041,13 @@ uint32_t zend_add_member_modifier(uint32_t flags, uint32_t new_flag, zend_modifi "Cannot use the final modifier on an abstract method", 0); return 0; } + if (target == ZEND_MODIFIER_TARGET_PROPERTY || target == ZEND_MODIFIER_TARGET_CPP) { + if ((flags & ZEND_ACC_PPP_SET_MASK) && (new_flag & ZEND_ACC_PPP_SET_MASK)) { + zend_throw_exception(zend_ce_compile_error, + "Multiple access type modifiers are not allowed", 0); + return 0; + } + } return new_flags; } /* }}} */ @@ -7569,7 +7597,7 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32 zend_string *name = zval_make_interned_string(zend_ast_get_zval(var_ast)); bool is_ref = (param_ast->attr & ZEND_PARAM_REF) != 0; bool is_variadic = (param_ast->attr & ZEND_PARAM_VARIADIC) != 0; - uint32_t property_flags = param_ast->attr & (ZEND_ACC_PPP_MASK | ZEND_ACC_READONLY); + uint32_t property_flags = param_ast->attr & (ZEND_ACC_PPP_MASK | ZEND_ACC_PPP_SET_MASK | ZEND_ACC_READONLY); bool is_promoted = property_flags || hooks_ast; znode var_node, default_node; @@ -7793,7 +7821,7 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32 zend_ast *param_ast = list->child[i]; zend_ast *hooks_ast = param_ast->child[5]; bool is_ref = (param_ast->attr & ZEND_PARAM_REF) != 0; - uint32_t flags = param_ast->attr & (ZEND_ACC_PPP_MASK | ZEND_ACC_READONLY); + uint32_t flags = param_ast->attr & (ZEND_ACC_PPP_MASK | ZEND_ACC_PPP_SET_MASK | ZEND_ACC_READONLY); bool is_promoted = flags || hooks_ast; if (!is_promoted) { continue; diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index c7e31877b5c..92a31764a89 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -253,7 +253,7 @@ typedef struct _zend_oparray_context { /* or IS_CONSTANT_VISITED_MARK | | | */ #define ZEND_CLASS_CONST_IS_CASE (1 << 6) /* | | | X */ /* | | | */ -/* Property Flags (unused: 10...) | | | */ +/* Property Flags (unused: 13...) | | | */ /* =========== | | | */ /* | | | */ /* Promoted property / parameter | | | */ @@ -262,6 +262,11 @@ typedef struct _zend_oparray_context { /* Virtual property without backing storage | | | */ #define ZEND_ACC_VIRTUAL (1 << 9) /* | | X | */ /* | | | */ +/* Asymmetric visibility | | | */ +#define ZEND_ACC_PUBLIC_SET (1 << 10) /* | | X | */ +#define ZEND_ACC_PROTECTED_SET (1 << 11) /* | | X | */ +#define ZEND_ACC_PRIVATE_SET (1 << 12) /* | | X | */ +/* | | | */ /* Class Flags (unused: 30,31) | | | */ /* =========== | | | */ /* | | | */ @@ -395,6 +400,20 @@ typedef struct _zend_oparray_context { #define ZEND_ACC_PPP_MASK (ZEND_ACC_PUBLIC | ZEND_ACC_PROTECTED | ZEND_ACC_PRIVATE) +#define ZEND_ACC_PPP_SET_MASK (ZEND_ACC_PUBLIC_SET | ZEND_ACC_PROTECTED_SET | ZEND_ACC_PRIVATE_SET) + +static zend_always_inline uint32_t zend_visibility_to_set_visibility(uint32_t visibility) +{ + switch (visibility) { + case ZEND_ACC_PUBLIC: + return ZEND_ACC_PUBLIC_SET; + case ZEND_ACC_PROTECTED: + return ZEND_ACC_PROTECTED_SET; + case ZEND_ACC_PRIVATE: + return ZEND_ACC_PRIVATE_SET; + EMPTY_SWITCH_DEFAULT_CASE(); + } +} /* call through internal function handler. e.g. Closure::invoke() */ #define ZEND_ACC_CALL_VIA_HANDLER ZEND_ACC_CALL_VIA_TRAMPOLINE @@ -1001,7 +1020,7 @@ ZEND_API zend_string *zend_type_to_string(zend_type type); #define ZEND_FETCH_CLASS_ALLOW_UNLINKED 0x0400 #define ZEND_FETCH_CLASS_ALLOW_NEARLY_LINKED 0x0800 -/* These should not clash with ZEND_ACC_(PUBLIC|PROTECTED|PRIVATE) */ +/* These should not clash with ZEND_ACC_PPP_MASK and ZEND_ACC_PPP_SET_MASK */ #define ZEND_PARAM_REF (1<<3) #define ZEND_PARAM_VARIADIC (1<<4) diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 602bb3b0e79..eb868760575 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -915,6 +915,32 @@ ZEND_API ZEND_COLD void ZEND_FASTCALL zend_object_released_while_assigning_to_pr ZSTR_VAL(info->ce->name), zend_get_unmangled_property_name(info->name)); } +ZEND_API ZEND_COLD void ZEND_FASTCALL zend_asymmetric_visibility_property_modification_error( + const zend_property_info *prop_info, const char *operation +) { + zend_class_entry *scope; + if (EG(fake_scope)) { + scope = EG(fake_scope); + } else { + scope = zend_get_called_scope(EG(current_execute_data)); + } + + const char *visibility; + if (prop_info->flags & ZEND_ACC_PRIVATE_SET) { + visibility = "private(set)"; + } else { + ZEND_ASSERT(prop_info->flags & ZEND_ACC_PROTECTED_SET); + visibility = "protected(set)"; + } + + zend_throw_error(NULL, "Cannot %s %s property %s::$%s from %s%s", + operation, + visibility, + ZSTR_VAL(prop_info->ce->name), + ZSTR_VAL(prop_info->name), + scope ? "scope " : "global scope", scope ? ZSTR_VAL(scope->name) : ""); +} + static const zend_class_entry *resolve_single_class_type(zend_string *name, const zend_class_entry *self_ce) { if (zend_string_equals_literal_ci(name, "self")) { return self_ce; @@ -1028,9 +1054,15 @@ static zend_never_inline zval* zend_assign_to_typed_prop(zend_property_info *inf { zval tmp; - if (UNEXPECTED((info->flags & ZEND_ACC_READONLY) && !(Z_PROP_FLAG_P(property_val) & IS_PROP_REINITABLE))) { - zend_readonly_property_modification_error(info); - return &EG(uninitialized_zval); + if (UNEXPECTED(info->flags & (ZEND_ACC_READONLY|ZEND_ACC_PPP_SET_MASK))) { + if ((info->flags & ZEND_ACC_READONLY) && !(Z_PROP_FLAG_P(property_val) & IS_PROP_REINITABLE)) { + zend_readonly_property_modification_error(info); + return &EG(uninitialized_zval); + } + if (info->flags & ZEND_ACC_PPP_SET_MASK && !zend_asymmetric_property_has_set_access(info)) { + zend_asymmetric_visibility_property_modification_error(info, "modify"); + return &EG(uninitialized_zval); + } } ZVAL_DEREF(value); @@ -3357,7 +3389,8 @@ static zend_always_inline void zend_fetch_property_address(zval *result, zval *c ZVAL_INDIRECT(result, ptr); zend_property_info *prop_info = CACHED_PTR_EX(cache_slot + 2); if (prop_info) { - if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) { + if (UNEXPECTED(prop_info->flags & (ZEND_ACC_READONLY|ZEND_ACC_PPP_SET_MASK)) + && ((prop_info->flags & ZEND_ACC_READONLY) || !zend_asymmetric_property_has_set_access(prop_info))) { /* For objects, W/RW/UNSET fetch modes might not actually modify object. * Similar as with magic __get() allow them, but return the value as a copy * to make sure no actual modification is possible. */ @@ -3365,7 +3398,11 @@ static zend_always_inline void zend_fetch_property_address(zval *result, zval *c if (Z_TYPE_P(ptr) == IS_OBJECT) { ZVAL_COPY(result, ptr); } else { - zend_readonly_property_indirect_modification_error(prop_info); + if (prop_info->flags & ZEND_ACC_READONLY) { + zend_readonly_property_indirect_modification_error(prop_info); + } else { + zend_asymmetric_visibility_property_modification_error(prop_info, "indirectly modify"); + } ZVAL_ERROR(result); } return; diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index fe854f30515..190d67f82a5 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -92,6 +92,9 @@ ZEND_API ZEND_COLD void ZEND_FASTCALL zend_object_released_while_assigning_to_pr ZEND_API ZEND_COLD void ZEND_FASTCALL zend_cannot_add_element(void); +ZEND_API bool ZEND_FASTCALL zend_asymmetric_property_has_set_access(const zend_property_info *prop_info); +ZEND_API ZEND_COLD void ZEND_FASTCALL zend_asymmetric_visibility_property_modification_error(const zend_property_info *info, const char *operation); + ZEND_API bool zend_verify_scalar_type_hint(uint32_t type_mask, zval *arg, bool strict, bool is_internal_arg); ZEND_API ZEND_COLD void zend_verify_arg_error( const zend_function *zf, const zend_arg_info *arg_info, uint32_t arg_num, zval *value); diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 2d6e0e42773..9d7c7f179de 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -212,6 +212,18 @@ char *zend_visibility_string(uint32_t fn_flags) /* {{{ */ } /* }}} */ +static const char *zend_asymmetric_visibility_string(uint32_t fn_flags) /* {{{ */ +{ + if (fn_flags & ZEND_ACC_PRIVATE_SET) { + return "private(set)"; + } else if (fn_flags & ZEND_ACC_PROTECTED_SET) { + return "protected(set)"; + } else { + ZEND_ASSERT(!(fn_flags & ZEND_ACC_PUBLIC_SET)); + return "omitted"; + } +} + static zend_string *resolve_class_name(zend_class_entry *scope, zend_string *name) { ZEND_ASSERT(scope); if (zend_string_equals_literal_ci(name, "parent") && scope->parent) { @@ -1442,6 +1454,25 @@ static void do_inherit_property(zend_property_info *parent_info, zend_string *ke ZSTR_VAL(ce->name), ZSTR_VAL(key)); } } + if (UNEXPECTED((child_info->flags & ZEND_ACC_PPP_SET_MASK)) + /* Get-only virtual properties have no set visibility, so any child visibility is fine. */ + && !(parent_info->hooks && (parent_info->flags & ZEND_ACC_VIRTUAL) && !parent_info->hooks[ZEND_PROPERTY_HOOK_SET])) { + uint32_t parent_set_visibility = parent_info->flags & ZEND_ACC_PPP_SET_MASK; + /* Adding set protection is fine if it's the same or weaker than + * the parents full property visibility. */ + if (!parent_set_visibility) { + parent_set_visibility = zend_visibility_to_set_visibility(parent_info->flags & ZEND_ACC_PPP_MASK); + } + uint32_t child_set_visibility = child_info->flags & ZEND_ACC_PPP_SET_MASK; + if (child_set_visibility > parent_set_visibility) { + zend_error_noreturn( + E_COMPILE_ERROR, + "Set access level of %s::$%s must be %s (as in class %s)%s", + ZSTR_VAL(ce->name), ZSTR_VAL(key), + zend_asymmetric_visibility_string(parent_info->flags), ZSTR_VAL(parent_info->ce->name), + !(parent_info->flags & ZEND_ACC_PPP_SET_MASK) ? "" : " or weaker"); + } + } if (UNEXPECTED((child_info->flags & ZEND_ACC_PPP_MASK) > (parent_info->flags & ZEND_ACC_PPP_MASK))) { zend_error_noreturn(E_COMPILE_ERROR, "Access level to %s::$%s must be %s (as in class %s)%s", ZSTR_VAL(ce->name), ZSTR_VAL(key), zend_visibility_string(parent_info->flags), ZSTR_VAL(parent_info->ce->name), (parent_info->flags&ZEND_ACC_PUBLIC) ? "" : " or weaker"); diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 5423d401857..d2a29e670d8 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -154,6 +154,9 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %token T_PRIVATE "'private'" %token T_PROTECTED "'protected'" %token T_PUBLIC "'public'" +%token T_PRIVATE_SET "'private(set)'" +%token T_PROTECTED_SET "'protected(set)'" +%token T_PUBLIC_SET "'public(set)'" %token T_READONLY "'readonly'" %token T_VAR "'var'" %token T_UNSET "'unset'" @@ -1066,6 +1069,9 @@ member_modifier: T_PUBLIC { $$ = T_PUBLIC; } | T_PROTECTED { $$ = T_PROTECTED; } | T_PRIVATE { $$ = T_PRIVATE; } + | T_PUBLIC_SET { $$ = T_PUBLIC_SET; } + | T_PROTECTED_SET { $$ = T_PROTECTED_SET; } + | T_PRIVATE_SET { $$ = T_PRIVATE_SET; } | T_STATIC { $$ = T_STATIC; } | T_ABSTRACT { $$ = T_ABSTRACT; } | T_FINAL { $$ = T_FINAL; } diff --git a/Zend/zend_language_scanner.l b/Zend/zend_language_scanner.l index 4551d26a17e..194f5a995fb 100644 --- a/Zend/zend_language_scanner.l +++ b/Zend/zend_language_scanner.l @@ -1725,6 +1725,18 @@ OPTIONAL_WHITESPACE_OR_COMMENTS ({WHITESPACE}|{MULTI_LINE_COMMENT}|{SINGLE_LINE_ RETURN_TOKEN_WITH_IDENT(T_PROTECTED); } +"public(set)" { + RETURN_TOKEN_WITH_IDENT(T_PUBLIC_SET); +} + +"protected(set)" { + RETURN_TOKEN_WITH_IDENT(T_PROTECTED_SET); +} + +"private(set)" { + RETURN_TOKEN_WITH_IDENT(T_PRIVATE_SET); +} + "public" { RETURN_TOKEN_WITH_IDENT(T_PUBLIC); } diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index 4c096d26b1b..fdac24ccb25 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -304,13 +304,6 @@ static ZEND_COLD zend_never_inline bool zend_deprecated_dynamic_property( return 1; } -static ZEND_COLD zend_never_inline void zend_readonly_property_modification_scope_error( - const zend_class_entry *ce, const zend_string *member, const zend_class_entry *scope, const char *operation) { - zend_throw_error(NULL, "Cannot %s readonly property %s::$%s from %s%s", - operation, ZSTR_VAL(ce->name), ZSTR_VAL(member), - scope ? "scope " : "global scope", scope ? ZSTR_VAL(scope->name) : ""); -} - static ZEND_COLD zend_never_inline void zend_readonly_property_unset_error( zend_class_entry *ce, zend_string *member) { zend_throw_error(NULL, "Cannot unset readonly property %s::$%s", @@ -547,6 +540,17 @@ ZEND_API zend_result zend_check_property_access(const zend_object *zobj, zend_st } /* }}} */ +ZEND_API bool ZEND_FASTCALL zend_asymmetric_property_has_set_access(const zend_property_info *prop_info) { + ZEND_ASSERT(prop_info->flags & ZEND_ACC_PPP_SET_MASK); + ZEND_ASSERT(!(prop_info->flags & ZEND_ACC_PUBLIC_SET)); + zend_class_entry *scope = get_fake_or_executed_scope(); + if (prop_info->ce == scope) { + return true; + } + return EXPECTED((prop_info->flags & ZEND_ACC_PROTECTED_SET) + && is_protected_compatible_scope(prop_info->ce, scope)); +} + static void zend_property_guard_dtor(zval *el) /* {{{ */ { uint32_t *ptr = (uint32_t*)Z_PTR_P(el); if (EXPECTED(!(((uintptr_t)ptr) & 1))) { @@ -686,32 +690,31 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int if (EXPECTED(IS_VALID_PROPERTY_OFFSET(property_offset))) { try_again: retval = OBJ_PROP(zobj, property_offset); - if (EXPECTED(Z_TYPE_P(retval) != IS_UNDEF)) { - if (prop_info && UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY) - && (type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET)) { - if (Z_TYPE_P(retval) == IS_OBJECT) { - /* For objects, W/RW/UNSET fetch modes might not actually modify object. - * Similar as with magic __get() allow them, but return the value as a copy - * to make sure no actual modification is possible. */ - ZVAL_COPY(rv, retval); - retval = rv; - } else { - zend_readonly_property_indirect_modification_error(prop_info); - retval = &EG(uninitialized_zval); - } + + if (prop_info && UNEXPECTED(prop_info->flags & (ZEND_ACC_READONLY|ZEND_ACC_PPP_SET_MASK)) + && (type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET) + && ((prop_info->flags & ZEND_ACC_READONLY) || !zend_asymmetric_property_has_set_access(prop_info))) { + if (Z_TYPE_P(retval) == IS_OBJECT) { + /* For objects, W/RW/UNSET fetch modes might not actually modify object. + * Similar as with magic __get() allow them, but return the value as a copy + * to make sure no actual modification is possible. */ + ZVAL_COPY(rv, retval); + retval = rv; + goto exit; + } else if (Z_TYPE_P(retval) == IS_UNDEF && type == BP_VAR_UNSET) { + retval = &EG(uninitialized_zval); + goto exit; } + if (prop_info->flags & ZEND_ACC_READONLY) { + zend_readonly_property_indirect_modification_error(prop_info); + } else { + zend_asymmetric_visibility_property_modification_error(prop_info, "indirectly modify"); + } + retval = &EG(uninitialized_zval); + goto exit; + } + if (EXPECTED(Z_TYPE_P(retval) != IS_UNDEF)) { goto exit; - } else { - if (prop_info && UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) { - if (type == BP_VAR_W || type == BP_VAR_RW) { - zend_readonly_property_indirect_modification_error(prop_info); - retval = &EG(uninitialized_zval); - goto exit; - } else if (type == BP_VAR_UNSET) { - retval = &EG(uninitialized_zval); - goto exit; - } - } } if (UNEXPECTED(Z_PROP_FLAG_P(retval) & IS_PROP_UNINIT)) { /* Skip __get() for uninitialized typed properties */ @@ -912,36 +915,12 @@ static zend_always_inline bool property_uses_strict_types(void) { && ZEND_CALL_USES_STRICT_TYPES(EG(current_execute_data)); } -static bool verify_readonly_initialization_access( - const zend_property_info *prop_info, const zend_class_entry *ce, - zend_string *name, const char *operation) { - zend_class_entry *scope = get_fake_or_executed_scope(); - if (prop_info->ce == scope) { - return true; - } - - /* We may have redeclared a parent property. In that case the parent should still be - * allowed to initialize it. */ - if (scope && is_derived_class(ce, scope)) { - const zend_property_info *prop_info = zend_hash_find_ptr(&scope->properties_info, name); - if (prop_info) { - /* This should be ensured by inheritance. */ - ZEND_ASSERT(prop_info->flags & ZEND_ACC_READONLY); - if (prop_info->ce == scope) { - return true; - } - } - } - - zend_readonly_property_modification_scope_error(prop_info->ce, name, scope, operation); - return false; -} - ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zval *value, void **cache_slot) /* {{{ */ { zval *variable_ptr, tmp; uintptr_t property_offset; const zend_property_info *prop_info = NULL; + uint32_t *guard = NULL; ZEND_ASSERT(!Z_ISREF_P(value)); property_offset = zend_get_property_offset(zobj->ce, name, (zobj->ce->__set != NULL), cache_slot, &prop_info); @@ -949,17 +928,35 @@ ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zva if (EXPECTED(IS_VALID_PROPERTY_OFFSET(property_offset))) { try_again: variable_ptr = OBJ_PROP(zobj, property_offset); - if (Z_TYPE_P(variable_ptr) != IS_UNDEF) { - Z_TRY_ADDREF_P(value); - if (prop_info) { - if (UNEXPECTED((prop_info->flags & ZEND_ACC_READONLY) && !(Z_PROP_FLAG_P(variable_ptr) & IS_PROP_REINITABLE))) { - Z_TRY_DELREF_P(value); + if (prop_info && UNEXPECTED(prop_info->flags & (ZEND_ACC_READONLY|ZEND_ACC_PPP_SET_MASK))) { + bool error; + if (Z_TYPE_P(variable_ptr) != IS_UNDEF || (Z_PROP_FLAG_P(variable_ptr) & IS_PROP_UNINIT) || !zobj->ce->__set) { + error = true; + } else { + guard = zend_get_property_guard(zobj, name); + error = (*guard) & IN_SET; + } + if (error) { + if ((prop_info->flags & ZEND_ACC_READONLY) + && Z_TYPE_P(variable_ptr) != IS_UNDEF + && !(Z_PROP_FLAG_P(variable_ptr) & IS_PROP_REINITABLE)) { zend_readonly_property_modification_error(prop_info); variable_ptr = &EG(error_zval); goto exit; } + if ((prop_info->flags & ZEND_ACC_PPP_SET_MASK) && !zend_asymmetric_property_has_set_access(prop_info)) { + zend_asymmetric_visibility_property_modification_error(prop_info, "modify"); + variable_ptr = &EG(error_zval); + goto exit; + } + } + } + if (Z_TYPE_P(variable_ptr) != IS_UNDEF) { + Z_TRY_ADDREF_P(value); + + if (prop_info) { typed_property: ZVAL_COPY_VALUE(&tmp, value); // Increase refcount to prevent object from being released in __toString() @@ -1073,7 +1070,9 @@ found:; /* magic set */ if (zobj->ce->__set) { - uint32_t *guard = zend_get_property_guard(zobj, name); + if (!guard) { + guard = zend_get_property_guard(zobj, name); + } if (!((*guard) & IN_SET)) { GC_ADDREF(zobj); @@ -1099,12 +1098,6 @@ write_std_property: Z_TRY_ADDREF_P(value); if (prop_info) { - if (UNEXPECTED((prop_info->flags & ZEND_ACC_READONLY) - && !verify_readonly_initialization_access(prop_info, zobj->ce, name, "initialize"))) { - Z_TRY_DELREF_P(value); - variable_ptr = &EG(error_zval); - goto exit; - } goto typed_property; } @@ -1270,9 +1263,10 @@ ZEND_API zval *zend_std_get_property_ptr_ptr(zend_object *zobj, zend_string *nam ZVAL_NULL(retval); } } - } else if (prop_info && UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) { - /* Readonly property, delegate to read_property + write_property. */ - retval = NULL; + } else if (prop_info && UNEXPECTED(prop_info->flags & (ZEND_ACC_READONLY|ZEND_ACC_PPP_SET_MASK))) { + if ((prop_info->flags & ZEND_ACC_READONLY) || !zend_asymmetric_property_has_set_access(prop_info)) { + retval = NULL; + } } else if (!prop_info || !ZEND_TYPE_IS_SET(prop_info->type)) { ZVAL_NULL(retval); } @@ -1280,9 +1274,10 @@ ZEND_API zval *zend_std_get_property_ptr_ptr(zend_object *zobj, zend_string *nam /* we do have getter - fail and let it try again with usual get/set */ retval = NULL; } - } else if (prop_info && UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) { - /* Readonly property, delegate to read_property + write_property. */ - retval = NULL; + } else if (prop_info && UNEXPECTED(prop_info->flags & (ZEND_ACC_READONLY|ZEND_ACC_PPP_SET_MASK))) { + if ((prop_info->flags & ZEND_ACC_READONLY) || !zend_asymmetric_property_has_set_access(prop_info)) { + retval = NULL; + } } } else if (EXPECTED(IS_DYNAMIC_PROPERTY_OFFSET(property_offset))) { if (EXPECTED(zobj->properties)) { @@ -1327,21 +1322,36 @@ ZEND_API void zend_std_unset_property(zend_object *zobj, zend_string *name, void { uintptr_t property_offset; const zend_property_info *prop_info = NULL; + uint32_t *guard = NULL; property_offset = zend_get_property_offset(zobj->ce, name, (zobj->ce->__unset != NULL), cache_slot, &prop_info); if (EXPECTED(IS_VALID_PROPERTY_OFFSET(property_offset))) { zval *slot = OBJ_PROP(zobj, property_offset); - if (Z_TYPE_P(slot) != IS_UNDEF) { - if (UNEXPECTED(prop_info && (prop_info->flags & ZEND_ACC_READONLY))) { - if (Z_PROP_FLAG_P(slot) & IS_PROP_REINITABLE) { - Z_PROP_FLAG_P(slot) &= ~IS_PROP_REINITABLE; - } else { + if (prop_info && UNEXPECTED(prop_info->flags & (ZEND_ACC_READONLY|ZEND_ACC_PPP_SET_MASK))) { + bool error; + if (Z_TYPE_P(slot) != IS_UNDEF || Z_PROP_FLAG_P(slot) & IS_PROP_UNINIT || !zobj->ce->__unset) { + error = true; + } else { + guard = zend_get_property_guard(zobj, name); + error = (*guard) & IN_UNSET; + } + if (error) { + if ((prop_info->flags & ZEND_ACC_READONLY) + && Z_TYPE_P(slot) != IS_UNDEF + && !(Z_PROP_FLAG_P(slot) & IS_PROP_REINITABLE)) { zend_readonly_property_unset_error(prop_info->ce, name); return; } + if ((prop_info->flags & ZEND_ACC_PPP_SET_MASK) && !zend_asymmetric_property_has_set_access(prop_info)) { + zend_asymmetric_visibility_property_modification_error(prop_info, "unset"); + return; + } } + } + + if (Z_TYPE_P(slot) != IS_UNDEF) { if (UNEXPECTED(Z_ISREF_P(slot)) && (ZEND_DEBUG || ZEND_REF_HAS_TYPE_SOURCES(Z_REF_P(slot)))) { if (prop_info) { @@ -1358,11 +1368,6 @@ ZEND_API void zend_std_unset_property(zend_object *zobj, zend_string *name, void return; } if (UNEXPECTED(Z_PROP_FLAG_P(slot) & IS_PROP_UNINIT)) { - if (UNEXPECTED(prop_info && (prop_info->flags & ZEND_ACC_READONLY) - && !verify_readonly_initialization_access(prop_info, zobj->ce, name, "unset"))) { - return; - } - /* Reset the IS_PROP_UNINIT flag, if it exists and bypass __unset(). */ Z_PROP_FLAG_P(slot) = 0; return; @@ -1388,7 +1393,9 @@ ZEND_API void zend_std_unset_property(zend_object *zobj, zend_string *name, void /* magic unset */ if (zobj->ce->__unset) { - uint32_t *guard = zend_get_property_guard(zobj, name); + if (!guard) { + guard = zend_get_property_guard(zobj, name); + } if (!((*guard) & IN_UNSET)) { /* have unsetter - try with it! */ (*guard) |= IN_UNSET; /* prevent circular unsetting */ diff --git a/Zend/zend_object_handlers.h b/Zend/zend_object_handlers.h index 730b110eccd..8ec2164413d 100644 --- a/Zend/zend_object_handlers.h +++ b/Zend/zend_object_handlers.h @@ -310,6 +310,8 @@ ZEND_API zend_function *zend_get_property_hook_trampoline( const zend_property_info *prop_info, zend_property_hook_kind kind, zend_string *prop_name); +ZEND_API bool ZEND_FASTCALL zend_asymmetric_property_has_set_access(const zend_property_info *prop_info); + #define zend_release_properties(ht) do { \ if ((ht) && !(GC_FLAGS(ht) & GC_IMMUTABLE) && !GC_DELREF(ht)) { \ zend_array_destroy(ht); \ diff --git a/ext/opcache/jit/zend_jit_helpers.c b/ext/opcache/jit/zend_jit_helpers.c index 8ed378d7552..a1370ea0723 100644 --- a/ext/opcache/jit/zend_jit_helpers.c +++ b/ext/opcache/jit/zend_jit_helpers.c @@ -2648,6 +2648,22 @@ static void ZEND_FASTCALL zend_jit_assign_obj_helper(zend_object *zobj, zend_str } } +static zend_always_inline bool verify_readonly_and_avis(zval *property_val, zend_property_info *info, bool indirect) +{ + if (UNEXPECTED(info->flags & (ZEND_ACC_READONLY|ZEND_ACC_PPP_SET_MASK))) { + if ((info->flags & ZEND_ACC_READONLY) && !(Z_PROP_FLAG_P(property_val) & IS_PROP_REINITABLE)) { + zend_readonly_property_modification_error(info); + return false; + } + if ((info->flags & ZEND_ACC_PPP_SET_MASK) && !zend_asymmetric_property_has_set_access(info)) { + const char *operation = indirect ? "indirectly modify" : "modify"; + zend_asymmetric_visibility_property_modification_error(info, operation); + return false; + } + } + return true; +} + static void ZEND_FASTCALL zend_jit_assign_to_typed_prop(zval *property_val, zend_property_info *info, zval *value, zval *result) { zend_execute_data *execute_data = EG(current_execute_data); @@ -2661,8 +2677,7 @@ static void ZEND_FASTCALL zend_jit_assign_to_typed_prop(zval *property_val, zend value = &EG(uninitialized_zval); } - if (UNEXPECTED((info->flags & ZEND_ACC_READONLY) && !(Z_PROP_FLAG_P(property_val) & IS_PROP_REINITABLE))) { - zend_readonly_property_modification_error(info); + if (UNEXPECTED(!verify_readonly_and_avis(property_val, info, false))) { if (result) { ZVAL_UNDEF(result); } @@ -2723,8 +2738,7 @@ static void ZEND_FASTCALL zend_jit_assign_op_to_typed_prop(zval *zptr, zend_prop zend_execute_data *execute_data = EG(current_execute_data); zval z_copy; - if (UNEXPECTED((prop_info->flags & ZEND_ACC_READONLY) && !(Z_PROP_FLAG_P(zptr) & IS_PROP_REINITABLE))) { - zend_readonly_property_modification_error(prop_info); + if (UNEXPECTED(!verify_readonly_and_avis(zptr, prop_info, true))) { return; } @@ -2818,8 +2832,7 @@ static void ZEND_FASTCALL zend_jit_inc_typed_prop(zval *var_ptr, zend_property_i { ZEND_ASSERT(Z_TYPE_P(var_ptr) != IS_UNDEF); - if (UNEXPECTED((prop_info->flags & ZEND_ACC_READONLY) && !(Z_PROP_FLAG_P(var_ptr) & IS_PROP_REINITABLE))) { - zend_readonly_property_modification_error(prop_info); + if (UNEXPECTED(!verify_readonly_and_avis(var_ptr, prop_info, true))) { return; } @@ -2851,8 +2864,7 @@ static void ZEND_FASTCALL zend_jit_dec_typed_prop(zval *var_ptr, zend_property_i { ZEND_ASSERT(Z_TYPE_P(var_ptr) != IS_UNDEF); - if (UNEXPECTED((prop_info->flags & ZEND_ACC_READONLY) && !(Z_PROP_FLAG_P(var_ptr) & IS_PROP_REINITABLE))) { - zend_readonly_property_modification_error(prop_info); + if (UNEXPECTED(!verify_readonly_and_avis(var_ptr, prop_info, true))) { return; } @@ -2898,8 +2910,7 @@ static void ZEND_FASTCALL zend_jit_post_inc_typed_prop(zval *var_ptr, zend_prope { ZEND_ASSERT(Z_TYPE_P(var_ptr) != IS_UNDEF); - if (UNEXPECTED((prop_info->flags & ZEND_ACC_READONLY) && !(Z_PROP_FLAG_P(var_ptr) & IS_PROP_REINITABLE))) { - zend_readonly_property_modification_error(prop_info); + if (UNEXPECTED(!verify_readonly_and_avis(var_ptr, prop_info, true))) { if (result) { ZVAL_UNDEF(result); } @@ -2933,8 +2944,7 @@ static void ZEND_FASTCALL zend_jit_post_dec_typed_prop(zval *var_ptr, zend_prope { ZEND_ASSERT(Z_TYPE_P(var_ptr) != IS_UNDEF); - if (UNEXPECTED((prop_info->flags & ZEND_ACC_READONLY) && !(Z_PROP_FLAG_P(var_ptr) & IS_PROP_REINITABLE))) { - zend_readonly_property_modification_error(prop_info); + if (UNEXPECTED(!verify_readonly_and_avis(var_ptr, prop_info, true))) { if (result) { ZVAL_UNDEF(result); } diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index bb0caf477e5..49ddf40a0e7 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -13933,16 +13933,37 @@ static int zend_jit_fetch_obj(zend_jit_ctx *jit, && (!ce || ce_is_instanceof || (ce->ce_flags & (ZEND_ACC_HAS_TYPE_HINTS|ZEND_ACC_TRAIT)))) { uint32_t flags = opline->extended_value & ZEND_FETCH_OBJ_FLAGS; + ir_ref allowed_inputs = IR_UNUSED; + ir_ref forbidden_inputs = IR_UNUSED; + ir_ref prop_info_ref = ir_LOAD_A( ir_ADD_OFFSET(run_time_cache, (opline->extended_value & ~ZEND_FETCH_OBJ_FLAGS) + sizeof(void*) * 2)); ir_ref if_has_prop_info = ir_IF(prop_info_ref); ir_IF_TRUE_cold(if_has_prop_info); - ir_ref if_readonly = ir_IF( - ir_AND_U32(ir_LOAD_U32(ir_ADD_OFFSET(prop_info_ref, offsetof(zend_property_info, flags))), - ir_CONST_U32(ZEND_ACC_READONLY))); + ir_ref prop_flags = ir_LOAD_U32(ir_ADD_OFFSET(prop_info_ref, offsetof(zend_property_info, flags))); + ir_ref if_readonly_or_avis = ir_IF(ir_AND_U32(prop_flags, ir_CONST_U32(ZEND_ACC_READONLY|ZEND_ACC_PPP_SET_MASK))); + + ir_IF_FALSE(if_readonly_or_avis); + ir_END_list(allowed_inputs); + + ir_IF_TRUE_cold(if_readonly_or_avis); + + ir_ref if_readonly = ir_IF(ir_AND_U32(prop_flags, ir_CONST_U32(ZEND_ACC_READONLY))); ir_IF_TRUE(if_readonly); + ir_END_list(forbidden_inputs); + + ir_IF_FALSE(if_readonly); + ir_ref has_avis_access = ir_CALL_1(IR_BOOL, ir_CONST_FC_FUNC(zend_asymmetric_property_has_set_access), prop_info_ref); + ir_ref if_avis_access = ir_IF(has_avis_access); + ir_IF_TRUE(if_avis_access); + ir_END_list(allowed_inputs); + + ir_IF_FALSE(if_avis_access); + ir_END_list(forbidden_inputs); + + ir_MERGE_list(forbidden_inputs); ir_ref if_prop_obj = jit_if_Z_TYPE(jit, prop_addr, IS_OBJECT); ir_IF_TRUE(if_prop_obj); @@ -13955,19 +13976,27 @@ static int zend_jit_fetch_obj(zend_jit_ctx *jit, ir_IF_FALSE_cold(if_prop_obj); jit_SET_EX_OPLINE(jit, opline); + if_readonly = ir_IF(ir_AND_U32(prop_flags, ir_CONST_U32(ZEND_ACC_READONLY))); + ir_IF_TRUE(if_readonly); ir_CALL_1(IR_VOID, ir_CONST_FC_FUNC(zend_readonly_property_indirect_modification_error), prop_info_ref); jit_set_Z_TYPE_INFO(jit, res_addr, _IS_ERROR); ir_END_list(end_inputs); + ir_IF_FALSE(if_readonly); + ir_CALL_2(IR_VOID, ir_CONST_FC_FUNC(zend_asymmetric_visibility_property_modification_error), + prop_info_ref, ir_CONST_ADDR("indirectly modify")); + jit_set_Z_TYPE_INFO(jit, res_addr, _IS_ERROR); + ir_END_list(end_inputs); + + ir_MERGE_list(allowed_inputs); + if (flags == ZEND_FETCH_DIM_WRITE) { - ir_IF_FALSE_cold(if_readonly); jit_SET_EX_OPLINE(jit, opline); ir_CALL_2(IR_VOID, ir_CONST_FC_FUNC(zend_jit_check_array_promotion), prop_ref, prop_info_ref); ir_END_list(end_inputs); ir_IF_FALSE(if_has_prop_info); } else if (flags == ZEND_FETCH_REF) { - ir_IF_FALSE_cold(if_readonly); ir_CALL_3(IR_VOID, ir_CONST_FC_FUNC(zend_jit_create_typed_ref), prop_ref, prop_info_ref, @@ -13976,10 +14005,7 @@ static int zend_jit_fetch_obj(zend_jit_ctx *jit, ir_IF_FALSE(if_has_prop_info); } else { ZEND_ASSERT(flags == 0); - ir_IF_FALSE(if_has_prop_info); - ir_ref no_prop_info_path = ir_END(); - ir_IF_FALSE(if_readonly); - ir_MERGE_WITH(no_prop_info_path); + ir_MERGE_WITH_EMPTY_FALSE(if_has_prop_info); } } } else { @@ -14005,9 +14031,6 @@ static int zend_jit_fetch_obj(zend_jit_ctx *jit, ir_IF_TRUE(if_def); } if (opline->opcode == ZEND_FETCH_OBJ_W && (prop_info->flags & ZEND_ACC_READONLY)) { - if (!prop_type_ref) { - prop_type_ref = jit_Z_TYPE_INFO(jit, prop_addr); - } ir_ref if_prop_obj = jit_if_Z_TYPE(jit, prop_addr, IS_OBJECT); ir_IF_TRUE(if_prop_obj); ir_ref ref = jit_Z_PTR(jit, prop_addr); @@ -14023,6 +14046,28 @@ static int zend_jit_fetch_obj(zend_jit_ctx *jit, ir_END_list(end_inputs); goto result_fetched; + } else if (opline->opcode == ZEND_FETCH_OBJ_W && (prop_info->flags & ZEND_ACC_PPP_SET_MASK)) { + /* Readonly properties which are also asymmetric are never mutable indirectly, which is + * handled by the previous branch. */ + ir_ref has_access = ir_CALL_1(IR_BOOL, ir_CONST_FC_FUNC(zend_asymmetric_property_has_set_access), ir_CONST_ADDR(prop_info)); + + ir_ref if_access = ir_IF(has_access); + ir_IF_FALSE_cold(if_access); + + ir_ref if_prop_obj = jit_if_Z_TYPE(jit, prop_addr, IS_OBJECT); + ir_IF_TRUE(if_prop_obj); + ir_ref ref = jit_Z_PTR(jit, prop_addr); + jit_GC_ADDREF(jit, ref); + jit_set_Z_PTR(jit, res_addr, ref); + jit_set_Z_TYPE_INFO(jit, res_addr, IS_OBJECT_EX); + ir_END_list(end_inputs); + + ir_IF_FALSE_cold(if_prop_obj); + ir_CALL_2(IR_VOID, ir_CONST_FC_FUNC(zend_asymmetric_visibility_property_modification_error), + ir_CONST_ADDR(prop_info), ir_CONST_ADDR("indirectly modify")); + ir_END_list(end_inputs); + + ir_IF_TRUE(if_access); } if (opline->opcode == ZEND_FETCH_OBJ_W diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 1d26c80f222..1215fa8b1ad 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -940,6 +940,17 @@ static void _property_string(smart_str *str, zend_property_info *prop, const cha smart_str_appends(str, "protected "); break; } + switch (prop->flags & ZEND_ACC_PPP_SET_MASK) { + case ZEND_ACC_PRIVATE_SET: + smart_str_appends(str, "private(set) "); + break; + case ZEND_ACC_PROTECTED_SET: + smart_str_appends(str, "protected(set) "); + break; + case ZEND_ACC_PUBLIC_SET: + ZEND_UNREACHABLE(); + break; + } if (prop->flags & ZEND_ACC_STATIC) { smart_str_appends(str, "static "); } @@ -5679,6 +5690,16 @@ ZEND_METHOD(ReflectionProperty, isProtected) } /* }}} */ +ZEND_METHOD(ReflectionProperty, isPrivateSet) +{ + _property_check_flag(INTERNAL_FUNCTION_PARAM_PASSTHRU, ZEND_ACC_PRIVATE_SET); +} + +ZEND_METHOD(ReflectionProperty, isProtectedSet) +{ + _property_check_flag(INTERNAL_FUNCTION_PARAM_PASSTHRU, ZEND_ACC_PROTECTED_SET); +} + /* {{{ Returns whether this property is static */ ZEND_METHOD(ReflectionProperty, isStatic) { @@ -5727,7 +5748,7 @@ ZEND_METHOD(ReflectionProperty, getModifiers) { reflection_object *intern; property_reference *ref; - uint32_t keep_flags = ZEND_ACC_PPP_MASK | ZEND_ACC_STATIC | ZEND_ACC_READONLY | ZEND_ACC_ABSTRACT | ZEND_ACC_VIRTUAL; + uint32_t keep_flags = ZEND_ACC_PPP_MASK | ZEND_ACC_PPP_SET_MASK | ZEND_ACC_STATIC | ZEND_ACC_READONLY | ZEND_ACC_ABSTRACT | ZEND_ACC_VIRTUAL; if (zend_parse_parameters_none() == FAILURE) { RETURN_THROWS(); diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php index d6d9c9d715f..2bb9a9a5efa 100644 --- a/ext/reflection/php_reflection.stub.php +++ b/ext/reflection/php_reflection.stub.php @@ -444,6 +444,10 @@ class ReflectionProperty implements Reflector public const int IS_PRIVATE = UNKNOWN; /** @cvalue ZEND_ACC_ABSTRACT */ public const int IS_ABSTRACT = UNKNOWN; + /** @cvalue ZEND_ACC_PROTECTED_SET */ + public const int IS_PROTECTED_SET = UNKNOWN; + /** @cvalue ZEND_ACC_PRIVATE_SET */ + public const int IS_PRIVATE_SET = UNKNOWN; public string $name; public string $class; @@ -480,6 +484,10 @@ class ReflectionProperty implements Reflector /** @tentative-return-type */ public function isProtected(): bool {} + public function isPrivateSet(): bool {} + + public function isProtectedSet(): bool {} + /** @tentative-return-type */ public function isStatic(): bool {} diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h index 2f0a513044c..19812c409c0 100644 --- a/ext/reflection/php_reflection_arginfo.h +++ b/ext/reflection/php_reflection_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: d2a365f6f398bbcf3f2520c28d508ca63f08b5ff */ + * Stub hash: 28fde6ed0e247201ee25d608d27a4c5b0bb8f2f7 */ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_Reflection_getModifierNames, 0, 1, IS_ARRAY, 0) ZEND_ARG_TYPE_INFO(0, modifiers, IS_LONG, 0) @@ -371,6 +371,10 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionProperty_isProtected arginfo_class_ReflectionFunctionAbstract_inNamespace +#define arginfo_class_ReflectionProperty_isPrivateSet arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + +#define arginfo_class_ReflectionProperty_isProtectedSet arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + #define arginfo_class_ReflectionProperty_isStatic arginfo_class_ReflectionFunctionAbstract_inNamespace #define arginfo_class_ReflectionProperty_isReadOnly arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType @@ -785,6 +789,8 @@ ZEND_METHOD(ReflectionProperty, isInitialized); ZEND_METHOD(ReflectionProperty, isPublic); ZEND_METHOD(ReflectionProperty, isPrivate); ZEND_METHOD(ReflectionProperty, isProtected); +ZEND_METHOD(ReflectionProperty, isPrivateSet); +ZEND_METHOD(ReflectionProperty, isProtectedSet); ZEND_METHOD(ReflectionProperty, isStatic); ZEND_METHOD(ReflectionProperty, isReadOnly); ZEND_METHOD(ReflectionProperty, isDefault); @@ -1078,6 +1084,8 @@ static const zend_function_entry class_ReflectionProperty_methods[] = { ZEND_ME(ReflectionProperty, isPublic, arginfo_class_ReflectionProperty_isPublic, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionProperty, isPrivate, arginfo_class_ReflectionProperty_isPrivate, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionProperty, isProtected, arginfo_class_ReflectionProperty_isProtected, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionProperty, isPrivateSet, arginfo_class_ReflectionProperty_isPrivateSet, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionProperty, isProtectedSet, arginfo_class_ReflectionProperty_isProtectedSet, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionProperty, isStatic, arginfo_class_ReflectionProperty_isStatic, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionProperty, isReadOnly, arginfo_class_ReflectionProperty_isReadOnly, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionProperty, isDefault, arginfo_class_ReflectionProperty_isDefault, ZEND_ACC_PUBLIC) @@ -1515,6 +1523,18 @@ static zend_class_entry *register_class_ReflectionProperty(zend_class_entry *cla zend_declare_typed_class_constant(class_entry, const_IS_ABSTRACT_name, &const_IS_ABSTRACT_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); zend_string_release(const_IS_ABSTRACT_name); + zval const_IS_PROTECTED_SET_value; + ZVAL_LONG(&const_IS_PROTECTED_SET_value, ZEND_ACC_PROTECTED_SET); + zend_string *const_IS_PROTECTED_SET_name = zend_string_init_interned("IS_PROTECTED_SET", sizeof("IS_PROTECTED_SET") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_IS_PROTECTED_SET_name, &const_IS_PROTECTED_SET_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_IS_PROTECTED_SET_name); + + zval const_IS_PRIVATE_SET_value; + ZVAL_LONG(&const_IS_PRIVATE_SET_value, ZEND_ACC_PRIVATE_SET); + zend_string *const_IS_PRIVATE_SET_name = zend_string_init_interned("IS_PRIVATE_SET", sizeof("IS_PRIVATE_SET") - 1, 1); + zend_declare_typed_class_constant(class_entry, const_IS_PRIVATE_SET_name, &const_IS_PRIVATE_SET_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(const_IS_PRIVATE_SET_name); + zval property_name_default_value; ZVAL_UNDEF(&property_name_default_value); zend_string *property_name_name = zend_string_init("name", sizeof("name") - 1, 1); diff --git a/ext/reflection/tests/ReflectionClass_toString_005.phpt b/ext/reflection/tests/ReflectionClass_toString_005.phpt index 4d4178828cc..a43cd96d8b7 100644 --- a/ext/reflection/tests/ReflectionClass_toString_005.phpt +++ b/ext/reflection/tests/ReflectionClass_toString_005.phpt @@ -25,8 +25,8 @@ Class [ readonly class Foo ] { } - Properties [2] { - Property [ public readonly int $bar ] - Property [ public readonly int $baz ] + Property [ public protected(set) readonly int $bar ] + Property [ public protected(set) readonly int $baz ] } - Methods [0] { diff --git a/ext/reflection/tests/ReflectionEnumUnitCase_getEnum.phpt b/ext/reflection/tests/ReflectionEnumUnitCase_getEnum.phpt index 21a2bc82d76..f9c05d2f6e5 100644 --- a/ext/reflection/tests/ReflectionEnumUnitCase_getEnum.phpt +++ b/ext/reflection/tests/ReflectionEnumUnitCase_getEnum.phpt @@ -31,7 +31,7 @@ Class [ final class Foo implements UnitEnum ] { } - Properties [1] { - Property [ public readonly string $name ] + Property [ public protected(set) readonly string $name ] } - Methods [0] { diff --git a/ext/reflection/tests/ReflectionEnum_toString.phpt b/ext/reflection/tests/ReflectionEnum_toString.phpt index 00965e7b783..91ef587a9a3 100644 --- a/ext/reflection/tests/ReflectionEnum_toString.phpt +++ b/ext/reflection/tests/ReflectionEnum_toString.phpt @@ -31,7 +31,7 @@ Class [ final class Foo implements UnitEnum ] { } - Properties [1] { - Property [ public readonly string $name ] + Property [ public protected(set) readonly string $name ] } - Methods [0] { diff --git a/ext/reflection/tests/asymmetric_visibility_flags.phpt b/ext/reflection/tests/asymmetric_visibility_flags.phpt new file mode 100644 index 00000000000..e6b171d3e76 --- /dev/null +++ b/ext/reflection/tests/asymmetric_visibility_flags.phpt @@ -0,0 +1,34 @@ +--TEST-- +ReflectionProperty::is{Private,Protected}Set +--FILE-- +isPrivateSet()); + var_dump($reflectionProperty->isProtectedSet()); + var_dump(($reflectionProperty->getModifiers() & ReflectionProperty::IS_PRIVATE_SET) !== 0); + var_dump(($reflectionProperty->getModifiers() & ReflectionProperty::IS_PROTECTED_SET) !== 0); + echo $reflectionProperty; +} + +test('bar'); +test('baz'); + +?> +--EXPECT-- +bool(true) +bool(false) +bool(true) +bool(false) +Property [ public private(set) int $bar ] +bool(false) +bool(true) +bool(false) +bool(true) +Property [ public protected(set) int $baz ] diff --git a/ext/reflection/tests/asymmetric_visibility_write.phpt b/ext/reflection/tests/asymmetric_visibility_write.phpt new file mode 100644 index 00000000000..6a582392fed --- /dev/null +++ b/ext/reflection/tests/asymmetric_visibility_write.phpt @@ -0,0 +1,25 @@ +--TEST-- +ReflectionProperty::is{Private,Protected}Set +--FILE-- +setValue($foo, $i++); + var_dump($reflectionProperty->getValue($foo)); +} + +test('bar'); +test('baz'); + +?> +--EXPECT-- +int(0) +int(1) diff --git a/ext/reflection/tests/readonly_properties.phpt b/ext/reflection/tests/readonly_properties.phpt index 25b3adcca36..dd1a1a06f2b 100644 --- a/ext/reflection/tests/readonly_properties.phpt +++ b/ext/reflection/tests/readonly_properties.phpt @@ -25,4 +25,4 @@ bool(false) bool(false) bool(true) bool(true) -Property [ public readonly int $ro ] +Property [ public protected(set) readonly int $ro ] diff --git a/ext/standard/tests/directory/DirectoryClass_basic_001.phpt b/ext/standard/tests/directory/DirectoryClass_basic_001.phpt index d02eeb96477..c345ea30b84 100644 --- a/ext/standard/tests/directory/DirectoryClass_basic_001.phpt +++ b/ext/standard/tests/directory/DirectoryClass_basic_001.phpt @@ -36,8 +36,8 @@ Class [ class Directory ] { } - Properties [2] { - Property [ public readonly string $path ] - Property [ public readonly mixed $handle ] + Property [ public protected(set) readonly string $path ] + Property [ public protected(set) readonly mixed $handle ] } - Methods [3] { diff --git a/ext/tokenizer/tokenizer_data.c b/ext/tokenizer/tokenizer_data.c index cdaaddecd7b..a046ab50e14 100644 --- a/ext/tokenizer/tokenizer_data.c +++ b/ext/tokenizer/tokenizer_data.c @@ -92,6 +92,9 @@ char *get_token_type_name(int token_type) case T_PRIVATE: return "T_PRIVATE"; case T_PROTECTED: return "T_PROTECTED"; case T_PUBLIC: return "T_PUBLIC"; + case T_PRIVATE_SET: return "T_PRIVATE_SET"; + case T_PROTECTED_SET: return "T_PROTECTED_SET"; + case T_PUBLIC_SET: return "T_PUBLIC_SET"; case T_READONLY: return "T_READONLY"; case T_VAR: return "T_VAR"; case T_UNSET: return "T_UNSET"; diff --git a/ext/tokenizer/tokenizer_data.stub.php b/ext/tokenizer/tokenizer_data.stub.php index 81e4e92626f..45f3c89f2de 100644 --- a/ext/tokenizer/tokenizer_data.stub.php +++ b/ext/tokenizer/tokenizer_data.stub.php @@ -337,6 +337,21 @@ const T_PROTECTED = UNKNOWN; * @cvalue T_PUBLIC */ const T_PUBLIC = UNKNOWN; +/** + * @var int + * @cvalue T_PRIVATE_SET + */ +const T_PRIVATE_SET = UNKNOWN; +/** + * @var int + * @cvalue T_PROTECTED_SET + */ +const T_PROTECTED_SET = UNKNOWN; +/** + * @var int + * @cvalue T_PUBLIC_SET + */ +const T_PUBLIC_SET = UNKNOWN; /** * @var int * @cvalue T_READONLY diff --git a/ext/tokenizer/tokenizer_data_arginfo.h b/ext/tokenizer/tokenizer_data_arginfo.h index 0fc1f219619..61f6ac1ec36 100644 --- a/ext/tokenizer/tokenizer_data_arginfo.h +++ b/ext/tokenizer/tokenizer_data_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: b7a13b3b242bd535f62a7d819eb0df751efb54ed */ + * Stub hash: d917cab61a2b436a16d2227cdb438add45e42d69 */ static void register_tokenizer_data_symbols(int module_number) { @@ -70,6 +70,9 @@ static void register_tokenizer_data_symbols(int module_number) REGISTER_LONG_CONSTANT("T_PRIVATE", T_PRIVATE, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_PROTECTED", T_PROTECTED, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_PUBLIC", T_PUBLIC, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("T_PRIVATE_SET", T_PRIVATE_SET, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("T_PROTECTED_SET", T_PROTECTED_SET, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("T_PUBLIC_SET", T_PUBLIC_SET, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_READONLY", T_READONLY, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_VAR", T_VAR, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_UNSET", T_UNSET, CONST_PERSISTENT);