diff --git a/Zend/tests/readonly_classes/readonly_class_inheritance_success.phpt b/Zend/tests/readonly_classes/readonly_class_inheritance_success.phpt index 33e73554ce01..133152ca6e91 100644 --- a/Zend/tests/readonly_classes/readonly_class_inheritance_success.phpt +++ b/Zend/tests/readonly_classes/readonly_class_inheritance_success.phpt @@ -5,11 +5,22 @@ Readonly class can extend a readonly class readonly class Foo { + public int $prop = 1; } readonly class Bar extends Foo { + public int $prop = 2; } +readonly class Baz extends Foo {} + +var_dump(new Foo()->prop); +var_dump(new Bar()->prop); +var_dump(new Baz()->prop); + ?> --EXPECT-- +int(1) +int(2) +int(1) diff --git a/Zend/tests/readonly_classes/readonly_with_property_default.phpt b/Zend/tests/readonly_classes/readonly_with_property_default.phpt new file mode 100644 index 000000000000..4268ff5734a4 --- /dev/null +++ b/Zend/tests/readonly_classes/readonly_with_property_default.phpt @@ -0,0 +1,36 @@ +--TEST-- +Properties of a readonly class may have default values +--FILE-- +bar = 2; + } catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + } + } +} + +$foo = new Foo(); +var_dump($foo->bar); +var_dump($foo->nullable); + +try { + $foo->bar = 3; +} catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +Error: Cannot modify readonly property Foo::$bar +int(1) +NULL +Error: Cannot modify readonly property Foo::$bar diff --git a/Zend/tests/readonly_classes/readonly_with_property_default_trait.phpt b/Zend/tests/readonly_classes/readonly_with_property_default_trait.phpt new file mode 100644 index 000000000000..e96945e0992e --- /dev/null +++ b/Zend/tests/readonly_classes/readonly_with_property_default_trait.phpt @@ -0,0 +1,18 @@ +--TEST-- +Readonly class may use readonly trait property with default value +--FILE-- +prop); + +?> +--EXPECT-- +int(2) diff --git a/Zend/tests/readonly_props/readonly_clone_success1.phpt b/Zend/tests/readonly_props/readonly_clone_success1.phpt index 72cd9e9622b3..ab6ee3157332 100644 --- a/Zend/tests/readonly_props/readonly_clone_success1.phpt +++ b/Zend/tests/readonly_props/readonly_clone_success1.phpt @@ -23,6 +23,24 @@ var_dump($foo2); var_dump(clone $foo2); +class FooWithDefault { + public readonly int $bar = 1; + + public function __clone() + { + $this->bar++; + } +} + +$fooWithDefault = new FooWithDefault(); + +var_dump(clone $fooWithDefault); + +$fooWithDefault2 = clone $fooWithDefault; +var_dump($fooWithDefault2); + +var_dump(clone $fooWithDefault2); + ?> --EXPECTF-- object(Foo)#%d (%d) { @@ -37,3 +55,15 @@ object(Foo)#%d (%d) { ["bar"]=> int(3) } +object(FooWithDefault)#%d (%d) { + ["bar"]=> + int(2) +} +object(FooWithDefault)#%d (%d) { + ["bar"]=> + int(2) +} +object(FooWithDefault)#%d (%d) { + ["bar"]=> + int(3) +} diff --git a/Zend/tests/readonly_props/readonly_modification.phpt b/Zend/tests/readonly_props/readonly_modification.phpt index bd04a203be19..2e13ad3fb408 100644 --- a/Zend/tests/readonly_props/readonly_modification.phpt +++ b/Zend/tests/readonly_props/readonly_modification.phpt @@ -14,6 +14,18 @@ class Test { } } +class TestWithDefault { + public readonly int $prop = 1; + + public function __construct() { + try { + $this->prop = 2; + } catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + } + } +} + function byRef(&$ref) {} $test = new Test; @@ -66,6 +78,8 @@ try { echo $e->getMessage(), "\n"; } +var_dump(new TestWithDefault()->prop); + ?> --EXPECT-- int(1) @@ -80,3 +94,5 @@ array(0) { } Cannot indirectly modify readonly property Test::$prop2 Cannot indirectly modify readonly property Test::$prop2 +Error: Cannot modify readonly property TestWithDefault::$prop +int(1) diff --git a/Zend/tests/readonly_props/readonly_trait_match.phpt b/Zend/tests/readonly_props/readonly_trait_match.phpt index 00aa6349aa14..36f2e11ec2bc 100644 --- a/Zend/tests/readonly_props/readonly_trait_match.phpt +++ b/Zend/tests/readonly_props/readonly_trait_match.phpt @@ -13,7 +13,20 @@ class C { use T1, T2; } +trait TDefault1 { + public readonly int $prop = 1; +} +trait TDefault2 { + public readonly int $prop = 1; +} +class CDefault { + use TDefault1, TDefault2; +} + +var_dump(new CDefault()->prop); + ?> ===DONE=== --EXPECT-- +int(1) ===DONE=== diff --git a/Zend/tests/readonly_props/readonly_with_default.phpt b/Zend/tests/readonly_props/readonly_with_default.phpt index 12afe5cde153..f8e381576e56 100644 --- a/Zend/tests/readonly_props/readonly_with_default.phpt +++ b/Zend/tests/readonly_props/readonly_with_default.phpt @@ -3,17 +3,43 @@ Readonly property with default value --FILE-- 2]; + public readonly E $enum = E::Case; + public readonly string $enumString = E::Case->name; } $test = new Test; +var_dump($test->prop); +var_dump($test->className); +var_dump($test->nullable); +var_dump($test->array); +var_dump($test->enum); +var_dump($test->enumString); try { $test->prop = 2; } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ": ", $e->getMessage(), "\n"; } ?> ---EXPECTF-- -Fatal error: Readonly property Test::$prop cannot have default value in %s on line %d +--EXPECT-- +int(1) +string(4) "Test" +NULL +array(2) { + [0]=> + int(1) + ["two"]=> + int(2) +} +enum(E::Case) +string(4) "Case" +Error: Cannot modify readonly property Test::$prop diff --git a/Zend/tests/readonly_props/readonly_with_default_asymmetric_visibility.phpt b/Zend/tests/readonly_props/readonly_with_default_asymmetric_visibility.phpt new file mode 100644 index 000000000000..a61373edc388 --- /dev/null +++ b/Zend/tests/readonly_props/readonly_with_default_asymmetric_visibility.phpt @@ -0,0 +1,33 @@ +--TEST-- +Readonly property with default value and asymmetric visibility +--FILE-- +default, $test->private, $test->protected, $test->public); + +foreach (['default', 'private', 'protected', 'public'] as $prop) { + try { + $test->$prop = 42; + } catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + } +} + +?> +--EXPECT-- +int(1) +int(2) +int(3) +int(4) +Error: Cannot modify readonly property Test::$default +Error: Cannot modify readonly property Test::$private +Error: Cannot modify readonly property Test::$protected +Error: Cannot modify readonly property Test::$public diff --git a/Zend/tests/readonly_props/readonly_with_default_inheritance.phpt b/Zend/tests/readonly_props/readonly_with_default_inheritance.phpt new file mode 100644 index 000000000000..8b285dcdd769 --- /dev/null +++ b/Zend/tests/readonly_props/readonly_with_default_inheritance.phpt @@ -0,0 +1,40 @@ +--TEST-- +Readonly property with default value and inheritance +--FILE-- +prop; + } +} + +class PrivateChild extends PrivateParent { + public readonly int $prop = 4; +} + +var_dump(new ChildInherits()->prop); +var_dump(new ChildOverrides()->prop); + +$privateChild = new PrivateChild(); +var_dump($privateChild->getParentProp()); +var_dump($privateChild->prop); + +?> +--EXPECT-- +int(1) +int(2) +int(3) +int(4) diff --git a/Zend/tests/readonly_props/readonly_with_default_interface.phpt b/Zend/tests/readonly_props/readonly_with_default_interface.phpt new file mode 100644 index 000000000000..a20d869dc661 --- /dev/null +++ b/Zend/tests/readonly_props/readonly_with_default_interface.phpt @@ -0,0 +1,29 @@ +--TEST-- +Readonly property with default value satisfies get-only interface property +--FILE-- +prop); + +interface J { + public int $prop { get; set; } +} + +// does not satisfy set +class D implements J { + public readonly int $prop = 42; +} + +?> +--EXPECTF-- +int(42) + +Fatal error: Set access level of D::$prop must be omitted (as in class J) in %s on line %d diff --git a/Zend/tests/readonly_props/readonly_with_default_reflection.phpt b/Zend/tests/readonly_props/readonly_with_default_reflection.phpt new file mode 100644 index 000000000000..b10191b202be --- /dev/null +++ b/Zend/tests/readonly_props/readonly_with_default_reflection.phpt @@ -0,0 +1,35 @@ +--TEST-- +Reflection for readonly property with default value +--FILE-- +isReadOnly()); +var_dump($rp->hasDefaultValue()); +var_dump($rp->getDefaultValue()); +var_dump(new ReflectionClass(Foo::class)->getDefaultProperties()); + +$test = new Foo(); +try { + $rp->setValue($test, 2); +} catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +bool(true) +bool(true) +int(1) +array(2) { + ["prop"]=> + int(1) + ["nullable"]=> + NULL +} +Error: Cannot modify readonly property Foo::$prop diff --git a/Zend/tests/readonly_props/readonly_with_default_trait_mismatch.phpt b/Zend/tests/readonly_props/readonly_with_default_trait_mismatch.phpt new file mode 100644 index 000000000000..6668b1d860bb --- /dev/null +++ b/Zend/tests/readonly_props/readonly_with_default_trait_mismatch.phpt @@ -0,0 +1,20 @@ +--TEST-- +Readonly trait property default value mismatch +--FILE-- + +--EXPECTF-- +Fatal error: T1 and T2 define the same property ($prop) in the composition of C. However, the definition differs and is considered incompatible. Class was composed in %s on line %d diff --git a/Zend/tests/readonly_props/serialization.phpt b/Zend/tests/readonly_props/serialization.phpt index f9e1f364673f..f51fce199a63 100644 --- a/Zend/tests/readonly_props/serialization.phpt +++ b/Zend/tests/readonly_props/serialization.phpt @@ -17,6 +17,18 @@ var_dump(unserialize($s)); var_dump(unserialize("O:4:\"Test\":1:{s:4:\"prop\";i:2;}")); var_dump(unserialize("O:4:\"Test\":2:{s:4:\"prop\";i:2;s:4:\"prop\";i:3;}")); +class TestDefault { + public readonly int $prop = 1; +} + +var_dump($s = serialize(new TestDefault)); +var_dump(unserialize($s)); + +// Unserialization still bypasses normal readonly assignment semantics. +var_dump(unserialize("O:11:\"TestDefault\":0:{}")); +var_dump(unserialize("O:11:\"TestDefault\":1:{s:4:\"prop\";i:2;}")); +var_dump(unserialize("O:11:\"TestDefault\":2:{s:4:\"prop\";i:2;s:4:\"prop\";i:3;}")); + ?> --EXPECT-- string(30) "O:4:"Test":1:{s:4:"prop";i:1;}" @@ -32,3 +44,20 @@ object(Test)#1 (1) { ["prop"]=> int(3) } +string(38) "O:11:"TestDefault":1:{s:4:"prop";i:1;}" +object(TestDefault)#1 (1) { + ["prop"]=> + int(1) +} +object(TestDefault)#1 (1) { + ["prop"]=> + int(1) +} +object(TestDefault)#1 (1) { + ["prop"]=> + int(2) +} +object(TestDefault)#1 (1) { + ["prop"]=> + int(3) +} diff --git a/Zend/tests/readonly_props/unset.phpt b/Zend/tests/readonly_props/unset.phpt index b8bd4218fa0c..f276ecaf2148 100644 --- a/Zend/tests/readonly_props/unset.phpt +++ b/Zend/tests/readonly_props/unset.phpt @@ -54,6 +54,33 @@ try { echo $e->getMessage(), "\n"; } +class Test4 { + public readonly int $prop = 1; + + public function __construct() { + try { + unset($this->prop); + } catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + } + } + + public function __get($name) { + echo __METHOD__, "\n"; // lazy pattern does not work + $this->prop = 2; + return $this->prop; + } +} + +$test = new Test4; +var_dump($test->prop); // Don't call __get. +try { + unset($test->prop); +} catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; +} +var_dump($test->prop); // Still don't call __get. + ?> --EXPECT-- Cannot unset readonly property Test::$prop @@ -62,3 +89,7 @@ int(1) int(1) Cannot unset readonly property Test2::$prop Cannot unset protected(set) readonly property Test3::$prop from global scope +Error: Cannot unset readonly property Test4::$prop +int(1) +Error: Cannot unset readonly property Test4::$prop +int(1) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index ee57e1dafb87..7b190680cfcc 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -9240,11 +9240,6 @@ static void zend_compile_prop_decl(zend_ast *ast, zend_ast *type_ast, uint32_t f zend_error_noreturn(E_COMPILE_ERROR, "Readonly property %s::$%s must have type", ZSTR_VAL(ce->name), ZSTR_VAL(name)); } - if (!Z_ISUNDEF(value_zv)) { - zend_error_noreturn(E_COMPILE_ERROR, - "Readonly property %s::$%s cannot have default value", - ZSTR_VAL(ce->name), ZSTR_VAL(name)); - } if (flags & ZEND_ACC_STATIC) { zend_error_noreturn(E_COMPILE_ERROR, "Static property %s::$%s cannot be readonly",