[RFC] Extend #[\Override] to target properties (#19061)

RFC: https://wiki.php.net/rfc/override_properties

Co-authored-by: Tim Düsterhus <tim@bastelstu.be>
This commit is contained in:
Jiří Pudil 2025-08-12 15:18:50 +02:00 committed by GitHub
parent c3bee21256
commit 38beb44176
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 449 additions and 19 deletions

1
NEWS
View file

@ -12,6 +12,7 @@ PHP NEWS
. The backtick operator as an alias for shell_exec() has been deprecated.
(timwolla)
. Returning null from __debugInfo() has been deprecated. (DanielEScherzer)
. Support #[\Override] on properties. (Jiří Pudil)
- Curl:
. The curl_close() function has been deprecated. (DanielEScherzer)

View file

@ -178,6 +178,8 @@ PHP 8.5 UPGRADE NOTES
RFC: https://wiki.php.net/rfc/pipe-operator-v3
. Constructor property promotion can now be used for final properties.
RFC: https://wiki.php.net/rfc/final_promotion
. #[\Override] can now be applied to properties.
RFC: https://wiki.php.net/rfc/override_properties
- Curl:
. Added support for share handles that are persisted across multiple PHP

View file

@ -0,0 +1,39 @@
--TEST--
#[\Override]: Properties
--FILE--
<?php
interface I {
public mixed $i { get; }
}
interface II extends I {
#[\Override]
public mixed $i { get; }
}
class P {
public mixed $p1;
public mixed $p2;
}
class PP extends P {
#[\Override]
public mixed $p1;
public mixed $p2;
}
class C extends PP implements I {
#[\Override]
public mixed $i;
#[\Override]
public mixed $p1;
public mixed $p2;
public mixed $c;
}
echo "Done";
?>
--EXPECT--
Done

View file

@ -0,0 +1,15 @@
--TEST--
#[\Override]: Properties: No parent class.
--FILE--
<?php
class C {
#[\Override]
public mixed $c;
}
echo "Done";
?>
--EXPECTF--
Fatal error: C::$c has #[\Override] attribute, but no matching parent property exists in %s on line %d

View file

@ -0,0 +1,21 @@
--TEST--
#[\Override]: Properties: No parent class, but child implements matching interface.
--FILE--
<?php
interface I {
public mixed $i { get; }
}
class P {
#[\Override]
public mixed $i;
}
class C extends P implements I {}
echo "Done";
?>
--EXPECTF--
Fatal error: P::$i has #[\Override] attribute, but no matching parent property exists in %s on line %d

View file

@ -0,0 +1,21 @@
--TEST--
#[\Override]: Properties: No parent class, but child implements matching interface (2).
--FILE--
<?php
interface I {
public mixed $i { get; }
}
class C extends P implements I {}
class P {
#[\Override]
public mixed $i;
}
echo "Done";
?>
--EXPECTF--
Fatal error: P::$i has #[\Override] attribute, but no matching parent property exists in %s on line %d

View file

@ -0,0 +1,26 @@
--TEST--
#[\Override]: Properties: No parent interface.
--FILE--
<?php
interface I {
#[\Override]
public mixed $i { get; }
}
interface II extends I {}
class C implements II {
public mixed $i;
}
class C2 implements I {
public mixed $i;
}
echo "Done";
?>
--EXPECTF--
Fatal error: I::$i has #[\Override] attribute, but no matching parent property exists in %s on line %d

View file

@ -0,0 +1,15 @@
--TEST--
#[\Override]: Properties: On trait.
--FILE--
<?php
trait T {
#[\Override]
public mixed $t;
}
echo "Done";
?>
--EXPECT--
Done

View file

@ -0,0 +1,19 @@
--TEST--
#[\Override]: Properties: On used trait without parent property.
--FILE--
<?php
trait T {
#[\Override]
public mixed $t;
}
class Foo {
use T;
}
echo "Done";
?>
--EXPECTF--
Fatal error: Foo::$t has #[\Override] attribute, but no matching parent property exists in %s on line %d

View file

@ -0,0 +1,23 @@
--TEST--
#[\Override]: Properties: On used trait with interface property.
--FILE--
<?php
trait T {
#[\Override]
public mixed $i;
}
interface I {
public mixed $i { get; }
}
class Foo implements I {
use T;
}
echo "Done";
?>
--EXPECT--
Done

View file

@ -0,0 +1,19 @@
--TEST--
#[\Override]: Properties: Parent property is private, child property is public.
--FILE--
<?php
class P {
private mixed $p;
}
class C extends P {
#[\Override]
public mixed $p;
}
echo "Done";
?>
--EXPECTF--
Fatal error: C::$p has #[\Override] attribute, but no matching parent property exists in %s on line %d

View file

@ -0,0 +1,19 @@
--TEST--
#[\Override]: Properties: Parent property is private, child property is private.
--FILE--
<?php
class P {
private mixed $p;
}
class C extends P {
#[\Override]
private mixed $p;
}
echo "Done";
?>
--EXPECTF--
Fatal error: C::$p has #[\Override] attribute, but no matching parent property exists in %s on line %d

View file

@ -0,0 +1,19 @@
--TEST--
#[\Override]: Properties: Parent property is protected, child property is public.
--FILE--
<?php
class P {
protected mixed $p;
}
class C extends P {
#[\Override]
public mixed $p;
}
echo "Done";
?>
--EXPECT--
Done

View file

@ -0,0 +1,19 @@
--TEST--
#[\Override]: Properties: Parent property is protected, child property is protected.
--FILE--
<?php
class P {
protected mixed $p;
}
class C extends P {
#[\Override]
protected mixed $p;
}
echo "Done";
?>
--EXPECT--
Done

View file

@ -0,0 +1,21 @@
--TEST--
#[\Override]: Properties: Redeclared trait property.
--FILE--
<?php
trait T {
public mixed $t;
}
class C {
use T;
#[\Override]
public mixed $t;
}
echo "Done";
?>
--EXPECTF--
Fatal error: C::$t has #[\Override] attribute, but no matching parent property exists in %s on line %d

View file

@ -0,0 +1,25 @@
--TEST--
#[\Override]: Properties: Redeclared trait property with interface.
--FILE--
<?php
interface I {
public mixed $i { get; }
}
trait T {
public mixed $i;
}
class C implements I {
use T;
#[\Override]
public mixed $i;
}
echo "Done";
?>
--EXPECT--
Done

View file

@ -0,0 +1,19 @@
--TEST--
#[\Override]: Properties: Valid anonymous class.
--FILE--
<?php
interface I {
public mixed $i { get; }
}
new class () implements I {
#[\Override]
public mixed $i;
};
echo "Done";
?>
--EXPECT--
Done

View file

@ -0,0 +1,21 @@
--TEST--
#[\Override]: Properties: Invalid anonymous class.
--FILE--
<?php
interface I {
public mixed $i { get; }
}
new class () implements I {
public mixed $i;
#[\Override]
public mixed $c;
};
echo "Done";
?>
--EXPECTF--
Fatal error: I@anonymous::$c has #[\Override] attribute, but no matching parent property exists in %s on line %d

View file

@ -0,0 +1,16 @@
--TEST--
#[\Override]: Properties: Static property no parent class.
--FILE--
<?php
class C
{
#[\Override]
public static mixed $c;
}
echo "Done";
?>
--EXPECTF--
Fatal error: C::$c has #[\Override] attribute, but no matching parent property exists in %s on line %d

View file

@ -0,0 +1,19 @@
--TEST--
#[\Override]: Properties: Static property.
--FILE--
<?php
class P {
public static mixed $p;
}
class C extends P {
#[\Override]
public static mixed $p;
}
echo "Done";
?>
--EXPECT--
Done

View file

@ -0,0 +1,21 @@
--TEST--
#[\Override]: Properties: valid promoted property
--FILE--
<?php
interface I {
public mixed $i { get; }
}
class C implements I {
public function __construct(
#[\Override]
public mixed $i,
) {}
}
echo "Done";
?>
--EXPECT--
Done

View file

@ -0,0 +1,17 @@
--TEST--
#[\Override]: Properties: invalid promoted property
--FILE--
<?php
class C {
public function __construct(
#[\Override]
public mixed $c,
) {}
}
echo "Done";
?>
--EXPECTF--
Fatal error: C::$c has #[\Override] attribute, but no matching parent property exists in %s on line %d

View file

@ -68,7 +68,7 @@ final class SensitiveParameterValue
/**
* @strict-properties
*/
#[Attribute(Attribute::TARGET_METHOD)]
#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_PROPERTY)]
final class Override
{
public function __construct() {}

View file

@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 9aee3d8f2ced376f5929048444eaa2529ff90311 */
* Stub hash: 715016d1ff1b0a6abb325a71083eff397a080c44 */
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Attribute___construct, 0, 0, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "Attribute::TARGET_ALL")
@ -230,7 +230,7 @@ static zend_class_entry *register_class_Override(void)
zend_string *attribute_name_Attribute_class_Override_0 = zend_string_init_interned("Attribute", sizeof("Attribute") - 1, 1);
zend_attribute *attribute_Attribute_class_Override_0 = zend_add_class_attribute(class_entry, attribute_name_Attribute_class_Override_0, 1);
zend_string_release(attribute_name_Attribute_class_Override_0);
ZVAL_LONG(&attribute_Attribute_class_Override_0->args[0].value, ZEND_ATTRIBUTE_TARGET_METHOD);
ZVAL_LONG(&attribute_Attribute_class_Override_0->args[0].value, ZEND_ATTRIBUTE_TARGET_METHOD | ZEND_ATTRIBUTE_TARGET_PROPERTY);
return class_entry;
}

View file

@ -7918,6 +7918,11 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32
if (attributes_ast) {
zend_compile_attributes(
&prop->attributes, attributes_ast, 0, ZEND_ATTRIBUTE_TARGET_PROPERTY, ZEND_ATTRIBUTE_TARGET_PARAMETER);
zend_attribute *override_attribute = zend_get_attribute_str(prop->attributes, "override", sizeof("override")-1);
if (override_attribute) {
prop->flags |= ZEND_ACC_OVERRIDE;
}
}
}
}
@ -8897,6 +8902,11 @@ static void zend_compile_prop_decl(zend_ast *ast, zend_ast *type_ast, uint32_t f
if (attr_ast) {
zend_compile_attributes(&info->attributes, attr_ast, 0, ZEND_ATTRIBUTE_TARGET_PROPERTY, 0);
zend_attribute *override_attribute = zend_get_attribute_str(info->attributes, "override", sizeof("override")-1);
if (override_attribute) {
info->flags |= ZEND_ACC_OVERRIDE;
}
}
CG(context).active_property_info_name = old_active_property_info_name;

View file

@ -254,7 +254,10 @@ typedef struct _zend_oparray_context {
/* or IS_CONSTANT_VISITED_MARK | | | */
#define ZEND_CLASS_CONST_IS_CASE (1 << 6) /* | | | X */
/* | | | */
/* Property Flags (unused: 13...) | | | */
/* has #[\Override] attribute | | | */
#define ZEND_ACC_OVERRIDE (1 << 28) /* | X | X | */
/* | | | */
/* Property Flags (unused: 13-27,29...) | | | */
/* =========== | | | */
/* | | | */
/* Promoted property / parameter | | | */
@ -393,9 +396,6 @@ typedef struct _zend_oparray_context {
/* supports opcache compile-time evaluation (funcs) | | | */
#define ZEND_ACC_COMPILE_TIME_EVAL (1 << 27) /* | X | | */
/* | | | */
/* has #[\Override] attribute | | | */
#define ZEND_ACC_OVERRIDE (1 << 28) /* | X | | */
/* | | | */
/* Has IS_PTR operands that needs special cleaning; same | | | */
/* value as ZEND_ACC_OVERRIDE but override is for class | | | */
/* methods and this is for the top level op array | | | */

View file

@ -1563,6 +1563,8 @@ static void do_inherit_property(zend_property_info *parent_info, zend_string *ke
ZSTR_VAL(key),
ZSTR_VAL(parent_info->ce->name));
}
child_info->flags &= ~ZEND_ACC_OVERRIDE;
}
} else {
zend_function **hooks = parent_info->hooks;
@ -2315,13 +2317,11 @@ static void zend_do_implement_interfaces(zend_class_entry *ce, zend_class_entry
void zend_inheritance_check_override(const zend_class_entry *ce)
{
zend_function *f;
if (ce->ce_flags & ZEND_ACC_TRAIT) {
return;
}
ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, f) {
ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, zend_function *f) {
if (f->common.fn_flags & ZEND_ACC_OVERRIDE) {
ZEND_ASSERT(f->type != ZEND_INTERNAL_FUNCTION);
@ -2332,14 +2332,17 @@ void zend_inheritance_check_override(const zend_class_entry *ce)
}
} ZEND_HASH_FOREACH_END();
if (ce->num_hooked_props) {
zend_property_info *prop;
ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, prop) {
if (!prop->hooks) {
continue;
}
ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, zend_property_info *prop) {
if (prop->flags & ZEND_ACC_OVERRIDE) {
zend_error_noreturn(
E_COMPILE_ERROR,
"%s::$%s has #[\\Override] attribute, but no matching parent property exists",
ZSTR_VAL(ce->name), zend_get_unmangled_property_name(prop->name));
}
if (prop->hooks) {
for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) {
f = prop->hooks[i];
zend_function *f = prop->hooks[i];
if (f && f->common.fn_flags & ZEND_ACC_OVERRIDE) {
ZEND_ASSERT(f->type != ZEND_INTERNAL_FUNCTION);
@ -2349,8 +2352,8 @@ void zend_inheritance_check_override(const zend_class_entry *ce)
ZEND_FN_SCOPE_NAME(f), ZSTR_VAL(f->common.function_name));
}
}
} ZEND_HASH_FOREACH_END();
}
}
} ZEND_HASH_FOREACH_END();
}