diff --git a/NEWS b/NEWS index b99bdc51e87c..5ac86fa1dda0 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,7 @@ PHP NEWS ?? ??? ????, PHP 8.6.0beta1 - Core: + . Allowed readonly properties to declare default values. (NickSdot) . Changed run-tests.php to run test subprocesses without a shell where possible. (NickSdot) . Fixed GH-23083 (SEGV build_trace_args in zend_exceptions.c with diff --git a/UPGRADING b/UPGRADING index ba4bf182911f..099edfab9ac7 100644 --- a/UPGRADING +++ b/UPGRADING @@ -291,6 +291,8 @@ PHP 8.6 UPGRADE NOTES ======================================== - Core: + . Readonly properties may now declare default values. + RFC: https://wiki.php.net/rfc/readonly_property_defaults . It is now possible to use reference assign on WeakMap without the key needing to be present beforehand. . It is now possible to define the __debugInfo() magic method on enums. 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..49d2bdf03a1e --- /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(), PHP_EOL; + } + } +} + +$foo = new Foo(); +var_dump($foo->bar); +var_dump($foo->nullable); + +try { + $foo->bar = 3; +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +?> +--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..9decd22a8baf --- /dev/null +++ b/Zend/tests/readonly_classes/readonly_with_property_default_trait.phpt @@ -0,0 +1,25 @@ +--TEST-- +Readonly class may use readonly trait property with default value +--FILE-- +prop); + +class B { + use TDefault; +} + +var_dump(new B()->prop); + +?> +--EXPECT-- +int(2) +int(2) diff --git a/Zend/tests/readonly_props/readonly_clone_success1.phpt b/Zend/tests/readonly_props/readonly_clone_success1.phpt index 72cd9e9622b3..50e5e6df81e2 100644 --- a/Zend/tests/readonly_props/readonly_clone_success1.phpt +++ b/Zend/tests/readonly_props/readonly_clone_success1.phpt @@ -23,6 +23,41 @@ 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); + +class FooWithDefaultCloneWith { + public readonly int $bar = 1; + + public function withBar(int $bar) + { + return clone($this, ['bar' => $bar]); + } +} + +$clone = new FooWithDefaultCloneWith(); +var_dump($clone); + +$clone2 = $clone->withBar(2); +var_dump($clone2); + +var_dump($clone2->withBar(0)); + ?> --EXPECTF-- object(Foo)#%d (%d) { @@ -37,3 +72,27 @@ 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) +} +object(FooWithDefaultCloneWith)#%d (%d) { + ["bar"]=> + int(1) +} +object(FooWithDefaultCloneWith)#%d (%d) { + ["bar"]=> + int(2) +} +object(FooWithDefaultCloneWith)#%d (%d) { + ["bar"]=> + int(0) +} diff --git a/Zend/tests/readonly_props/readonly_modification.phpt b/Zend/tests/readonly_props/readonly_modification.phpt index bd04a203be19..661091375045 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(), PHP_EOL; + } + } +} + 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..affe62f8d1ff 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(), PHP_EOL; } ?> ---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_abstract_get_set_implicit_set.phpt b/Zend/tests/readonly_props/readonly_with_default_abstract_get_set_implicit_set.phpt new file mode 100644 index 000000000000..e61574d07aaf --- /dev/null +++ b/Zend/tests/readonly_props/readonly_with_default_abstract_get_set_implicit_set.phpt @@ -0,0 +1,18 @@ +--TEST-- +Readonly property with default value has restricted set visibility for get/set abstract property +--DESCRIPTION-- +The error message should be improved, the set access level comes from readonly. +--FILE-- + +--EXPECTF-- +Fatal error: Set access level of C::$prop must be omitted (as in class P) in %s on line %d 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..11b5a741ca17 --- /dev/null +++ b/Zend/tests/readonly_props/readonly_with_default_asymmetric_visibility.phpt @@ -0,0 +1,34 @@ +--TEST-- +Readonly property with default value and asymmetric visibility +--FILE-- +$prop; + try { + $test->$prop = 42; + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + echo "$$prop before $before, after {$test->$prop}", PHP_EOL; +} + +?> +--EXPECT-- +Error: Cannot modify readonly property Test::$default +$default before 1, after 1 +Error: Cannot modify readonly property Test::$private +$private before 2, after 2 +Error: Cannot modify readonly property Test::$protected +$protected before 3, after 3 +Error: Cannot modify readonly property Test::$public +$public before 4, after 4 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_get_only.phpt b/Zend/tests/readonly_props/readonly_with_default_interface_get_only.phpt new file mode 100644 index 000000000000..a9f7f56bcd2c --- /dev/null +++ b/Zend/tests/readonly_props/readonly_with_default_interface_get_only.phpt @@ -0,0 +1,17 @@ +--TEST-- +Readonly property with default value satisfies get-only interface property +--FILE-- +prop); +?> +--EXPECTF-- +int(42) diff --git a/Zend/tests/readonly_props/readonly_with_default_interface_get_set.phpt b/Zend/tests/readonly_props/readonly_with_default_interface_get_set.phpt new file mode 100644 index 000000000000..102444cc044f --- /dev/null +++ b/Zend/tests/readonly_props/readonly_with_default_interface_get_set.phpt @@ -0,0 +1,16 @@ +--TEST-- +Readonly public(set) property with default value does not satisfy get/set interface property +--FILE-- + +--EXPECTF-- +Fatal error: Class C contains 1 abstract method and must therefore be declared abstract or implement the remaining method (I::$prop::set) in %s on line %d diff --git a/Zend/tests/readonly_props/readonly_with_default_interface_get_set_implicit_set.phpt b/Zend/tests/readonly_props/readonly_with_default_interface_get_set_implicit_set.phpt new file mode 100644 index 000000000000..106bf2aeb0cf --- /dev/null +++ b/Zend/tests/readonly_props/readonly_with_default_interface_get_set_implicit_set.phpt @@ -0,0 +1,17 @@ +--TEST-- +Readonly property with default value has restricted set visibility for get/set interface property +--DESCRIPTION-- +The error message should be improved, the set access level comes from readonly. Ref: Zend/tests/property_hooks/interface_get_set_readonly.phpt +--FILE-- + +--EXPECTF-- +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/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..bf32fb7f0000 100644 --- a/Zend/tests/readonly_props/serialization.phpt +++ b/Zend/tests/readonly_props/serialization.phpt @@ -17,6 +17,43 @@ 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)); + +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;}")); + +class TestDefaultWithUnserialize { + public readonly int $prop = 1; + public public(set) readonly int $lock = 1; + + public function __unserialize(array $data): void { + foreach ($data as $key => $value) { + $this->{$key} = $value; + } + } +} + +$testDefaultWithUnserialize = unserialize("O:26:\"TestDefaultWithUnserialize\":1:{s:4:\"prop\";i:2;}"); +var_dump($testDefaultWithUnserialize); + +try { + $testDefaultWithUnserialize->prop = 3; +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +try { + $testDefaultWithUnserialize->lock = 3; +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + ?> --EXPECT-- string(30) "O:4:"Test":1:{s:4:"prop";i:1;}" @@ -32,3 +69,28 @@ 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) +} +object(TestDefaultWithUnserialize)#1 (2) { + ["prop"]=> + int(2) + ["lock"]=> + int(1) +} +Error: Cannot modify readonly property TestDefaultWithUnserialize::$prop +Error: Cannot modify readonly property TestDefaultWithUnserialize::$lock diff --git a/Zend/tests/readonly_props/unset.phpt b/Zend/tests/readonly_props/unset.phpt index b8bd4218fa0c..2164570f2475 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(), PHP_EOL; + } + } + + 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(), PHP_EOL; +} +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 a2f126fb101d..a0a9677a476a 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -9480,11 +9480,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", diff --git a/ext/reflection/tests/ReflectionClass_toString_009.phpt b/ext/reflection/tests/ReflectionClass_toString_009.phpt new file mode 100644 index 000000000000..b2d118c459b8 --- /dev/null +++ b/ext/reflection/tests/ReflectionClass_toString_009.phpt @@ -0,0 +1,32 @@ +--TEST-- +ReflectionClass::__toString() - readonly property with default +--FILE-- + +--EXPECTF-- +Class [ class Test ] { + @@ %s 3-5 + + - Constants [0] { + } + + - Static properties [0] { + } + + - Static methods [0] { + } + + - Properties [1] { + Property [ public protected(set) readonly int $property = 42 ] + } + + - Methods [0] { + } +} diff --git a/ext/reflection/tests/ReflectionProperty_readonly_with_default.phpt b/ext/reflection/tests/ReflectionProperty_readonly_with_default.phpt new file mode 100644 index 000000000000..d3c3e8a084e4 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_readonly_with_default.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(), PHP_EOL; +} + +?> +--EXPECT-- +bool(true) +bool(true) +int(1) +array(2) { + ["prop"]=> + int(1) + ["nullable"]=> + NULL +} +Error: Cannot modify readonly property Foo::$prop diff --git a/ext/reflection/tests/ReflectionProperty_toString_002.phpt b/ext/reflection/tests/ReflectionProperty_toString_002.phpt new file mode 100644 index 000000000000..5b93d2149b9a --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_toString_002.phpt @@ -0,0 +1,14 @@ +--TEST-- +ReflectionProperty::__toString() - readonly with default +--FILE-- + +--EXPECT-- +Property [ public protected(set) readonly int $nick = 42 ] diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re index eca9660c5605..1fb8793c2bbc 100644 --- a/ext/standard/var_unserializer.re +++ b/ext/standard/var_unserializer.re @@ -17,6 +17,7 @@ #include "php_incomplete_class.h" #include "zend_portability.h" #include "zend_exceptions.h" +#include "zend_objects.h" /* {{{ reference-handling for unserializer: var_* */ #define VAR_ENTRIES_MAX 1018 /* 1024 - offsetof(php_unserialize_data, entries) / sizeof(void*) */ @@ -300,6 +301,7 @@ PHPAPI void var_destroy(php_unserialize_data_t *var_hashx) zval param; ZVAL_COPY(¶m, &var_dtor_hash->data[i + 1]); + zend_object_set_properties_reinitable(Z_OBJ_P(zv), /* reinitable */ true); BG(serialize_lock)++; zend_call_known_instance_method_with_1_params( Z_OBJCE_P(zv)->__unserialize, Z_OBJ_P(zv), NULL, ¶m); @@ -308,6 +310,7 @@ PHPAPI void var_destroy(php_unserialize_data_t *var_hashx) GC_ADD_FLAGS(Z_OBJ_P(zv), IS_OBJ_DESTRUCTOR_CALLED); } BG(serialize_lock)--; + zend_object_set_properties_reinitable(Z_OBJ_P(zv), /* reinitable */ false); zval_ptr_dtor(¶m); } else { GC_ADD_FLAGS(Z_OBJ_P(zv), IS_OBJ_DESTRUCTOR_CALLED);