Commit 4629b3e1fa1 for php.net
commit 4629b3e1fa1abd5e720af5c3885756a975db1812
Author: Daniel Scherzer <daniel.e.scherzer@gmail.com>
Date: Fri Jul 3 13:03:27 2026 -0700
[RFC] Allow `#[\Override]` on class constants (#20478)
https://wiki.php.net/rfc/override_constants
diff --git a/UPGRADING b/UPGRADING
index a42d5ec355a..b9c659ccc40 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -215,6 +215,8 @@ PHP 8.6 UPGRADE NOTES
needing to be present beforehand.
. It is now possible to define the __debugInfo() magic method on enums.
RFC: https://wiki.php.net/rfc/debugable-enums
+ . #[\Override] can now be applied to class constants, including enum cases.
+ RFC: https://wiki.php.net/rfc/override_constants
- Curl:
. curl_getinfo() return array now includes a new size_delivered key, which
diff --git a/Zend/tests/attributes/delayed_target_validation/with_Override_error_constant.phpt b/Zend/tests/attributes/delayed_target_validation/with_Override_error_constant.phpt
new file mode 100644
index 00000000000..9ebb8e4c020
--- /dev/null
+++ b/Zend/tests/attributes/delayed_target_validation/with_Override_error_constant.phpt
@@ -0,0 +1,15 @@
+--TEST--
+#[\DelayedTargetValidation] with #[\Override]: non-overrides still error (class constant)
+--FILE--
+<?php
+
+class DemoClass {
+
+ #[DelayedTargetValidation]
+ #[Override] // Does something here
+ public const CLASS_CONSTANT = 'FOO';
+}
+
+?>
+--EXPECTF--
+Fatal error: DemoClass::CLASS_CONSTANT has #[\Override] attribute, but no matching parent constant exists in %s on line %d
diff --git a/Zend/tests/attributes/delayed_target_validation/with_Override_error_enum_case.phpt b/Zend/tests/attributes/delayed_target_validation/with_Override_error_enum_case.phpt
new file mode 100644
index 00000000000..53d75412a84
--- /dev/null
+++ b/Zend/tests/attributes/delayed_target_validation/with_Override_error_enum_case.phpt
@@ -0,0 +1,15 @@
+--TEST--
+#[\DelayedTargetValidation] with #[\Override]: non-overrides still error (enum case)
+--FILE--
+<?php
+
+enum DemoEnum {
+
+ #[DelayedTargetValidation]
+ #[Override] // Does something here
+ case MyCase;
+}
+
+?>
+--EXPECTF--
+Fatal error: DemoEnum::MyCase has #[\Override] attribute, but no matching parent constant exists in %s on line %d
diff --git a/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt b/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt
index dd077f4b9cb..494d85eaea9 100644
--- a/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt
+++ b/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt
@@ -12,6 +12,8 @@ class Base {
set => $value;
}
+ public const CLASS_CONST = '';
+
public function printVal() {
echo __METHOD__ . "\n";
}
@@ -34,7 +36,7 @@ class DemoClass extends Base {
}
#[DelayedTargetValidation]
- #[Override] // Does nothing here
+ #[Override] // Does something here
public const CLASS_CONST = 'FOO';
public function __construct(
diff --git a/Zend/tests/attributes/override/constants/anon_failure.phpt b/Zend/tests/attributes/override/constants/anon_failure.phpt
new file mode 100644
index 00000000000..c43cedda81d
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/anon_failure.phpt
@@ -0,0 +1,15 @@
+--TEST--
+#[\Override]: Constants - anonymous class, no interface or parent class
+--FILE--
+<?php
+
+new class () {
+ #[\Override]
+ public const C = 'C';
+};
+
+echo "Done";
+
+?>
+--EXPECTF--
+Fatal error: class@anonymous::C has #[\Override] attribute, but no matching parent constant exists in %s on line %d
diff --git a/Zend/tests/attributes/override/constants/anon_interface.phpt b/Zend/tests/attributes/override/constants/anon_interface.phpt
new file mode 100644
index 00000000000..b19abfbf4d3
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/anon_interface.phpt
@@ -0,0 +1,19 @@
+--TEST--
+#[\Override]: Constants - anonymous class overrides interface
+--FILE--
+<?php
+
+interface IFace {
+ public const I = 'I';
+}
+
+new class () implements IFace {
+ #[\Override]
+ public const I = 'Changed';
+};
+
+echo "Done";
+
+?>
+--EXPECT--
+Done
diff --git a/Zend/tests/attributes/override/constants/anon_parent.phpt b/Zend/tests/attributes/override/constants/anon_parent.phpt
new file mode 100644
index 00000000000..b430705c3c9
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/anon_parent.phpt
@@ -0,0 +1,19 @@
+--TEST--
+#[\Override]: Constants - anonymous class overrides parent class
+--FILE--
+<?php
+
+class Base {
+ public const C = 'C';
+}
+
+new class () extends Base {
+ #[\Override]
+ public const C = 'Changed';
+};
+
+echo "Done";
+
+?>
+--EXPECT--
+Done
diff --git a/Zend/tests/attributes/override/constants/basic.phpt b/Zend/tests/attributes/override/constants/basic.phpt
new file mode 100644
index 00000000000..c8900702205
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/basic.phpt
@@ -0,0 +1,57 @@
+--TEST--
+#[\Override]: Constants - basic
+--FILE--
+<?php
+
+interface I {
+ public const I = 'I';
+}
+
+interface II extends I {
+ #[\Override]
+ public const I = 'I';
+}
+
+class P {
+ public const C1 = 'C1';
+ public const C2 = 'C2';
+ public const C3 = 'C3';
+ public const C4 = 'C4';
+}
+
+class PP extends P {
+ #[\Override]
+ public const C1 = 'C1';
+ public const C2 = 'C2';
+ #[\Override]
+ public const C3 = 'C3';
+}
+
+class C extends PP implements I {
+ #[\Override]
+ public const I = 'I';
+ #[\Override]
+ public const C1 = 'C1';
+ #[\Override]
+ public const C2 = 'C2';
+ public const C3 = 'C3';
+ #[\Override]
+ public const C4 = 'C4';
+ public const C = 'C';
+}
+
+enum E implements I {
+ #[\Override]
+ public const I = 'I';
+}
+
+enum WithCase implements I {
+ #[\Override]
+ case I;
+}
+
+echo "Done";
+
+?>
+--EXPECT--
+Done
diff --git a/Zend/tests/attributes/override/constants/enum_failure.phpt b/Zend/tests/attributes/override/constants/enum_failure.phpt
new file mode 100644
index 00000000000..77ba225e9b9
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/enum_failure.phpt
@@ -0,0 +1,15 @@
+--TEST--
+#[\Override]: Constants - no interface for enum constant
+--FILE--
+<?php
+
+enum Demo {
+ #[\Override]
+ public const C = 'C';
+}
+
+echo "Done";
+
+?>
+--EXPECTF--
+Fatal error: Demo::C has #[\Override] attribute, but no matching parent constant exists in %s on line %d
diff --git a/Zend/tests/attributes/override/constants/enum_failure_case.phpt b/Zend/tests/attributes/override/constants/enum_failure_case.phpt
new file mode 100644
index 00000000000..2f2c6fc84a0
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/enum_failure_case.phpt
@@ -0,0 +1,15 @@
+--TEST--
+#[\Override]: Constants - no interface for enum case
+--FILE--
+<?php
+
+enum Demo {
+ #[\Override]
+ case C;
+}
+
+echo "Done";
+
+?>
+--EXPECTF--
+Fatal error: Demo::C has #[\Override] attribute, but no matching parent constant exists in %s on line %d
diff --git a/Zend/tests/attributes/override/constants/failure.phpt b/Zend/tests/attributes/override/constants/failure.phpt
new file mode 100644
index 00000000000..be621915d92
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/failure.phpt
@@ -0,0 +1,15 @@
+--TEST--
+#[\Override]: Constants - no interface or parent class
+--FILE--
+<?php
+
+class Demo {
+ #[\Override]
+ public const C = 'C';
+}
+
+echo "Done";
+
+?>
+--EXPECTF--
+Fatal error: Demo::C has #[\Override] attribute, but no matching parent constant exists in %s on line %d
diff --git a/Zend/tests/attributes/override/constants/interface_failure.phpt b/Zend/tests/attributes/override/constants/interface_failure.phpt
new file mode 100644
index 00000000000..0aa4d33d130
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/interface_failure.phpt
@@ -0,0 +1,15 @@
+--TEST--
+#[\Override]: Constants - no parent interface
+--FILE--
+<?php
+
+interface IFace {
+ #[\Override]
+ public const I = 'I';
+}
+
+echo "Done";
+
+?>
+--EXPECTF--
+Fatal error: IFace::I has #[\Override] attribute, but no matching parent constant exists in %s on line %d
diff --git a/Zend/tests/attributes/override/constants/trait_failure.phpt b/Zend/tests/attributes/override/constants/trait_failure.phpt
new file mode 100644
index 00000000000..8d4af0e5797
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/trait_failure.phpt
@@ -0,0 +1,19 @@
+--TEST--
+#[\Override]: Constants - on a trait, no interface or parent class
+--FILE--
+<?php
+
+trait DemoTrait {
+ #[\Override]
+ public const T = 'T';
+}
+
+class UsesTrait {
+ use DemoTrait;
+}
+
+echo "Done";
+
+?>
+--EXPECTF--
+Fatal error: UsesTrait::T has #[\Override] attribute, but no matching parent constant exists in %s on line %d
diff --git a/Zend/tests/attributes/override/constants/trait_interface.phpt b/Zend/tests/attributes/override/constants/trait_interface.phpt
new file mode 100644
index 00000000000..3a2ba799ae4
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/trait_interface.phpt
@@ -0,0 +1,23 @@
+--TEST--
+#[\Override]: Constants - on a trait, overrides interface
+--FILE--
+<?php
+
+trait DemoTrait {
+ #[\Override]
+ public const C = 'Changed';
+}
+
+interface IFace {
+ public const C = 'C';
+}
+
+class UsesTrait implements IFace {
+ use DemoTrait;
+}
+
+echo "Done";
+
+?>
+--EXPECT--
+Done
diff --git a/Zend/tests/attributes/override/constants/trait_parent.phpt b/Zend/tests/attributes/override/constants/trait_parent.phpt
new file mode 100644
index 00000000000..2753aaf546a
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/trait_parent.phpt
@@ -0,0 +1,23 @@
+--TEST--
+#[\Override]: Constants - on a trait, overrides parent class
+--FILE--
+<?php
+
+trait DemoTrait {
+ #[\Override]
+ public const C = 'Changed';
+}
+
+class Base {
+ public const C = 'C';
+}
+
+class UsesTrait extends Base {
+ use DemoTrait;
+}
+
+echo "Done";
+
+?>
+--EXPECT--
+Done
diff --git a/Zend/tests/attributes/override/constants/trait_redeclared.phpt b/Zend/tests/attributes/override/constants/trait_redeclared.phpt
new file mode 100644
index 00000000000..984f9eedb66
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/trait_redeclared.phpt
@@ -0,0 +1,21 @@
+--TEST--
+#[\Override]: Constants - trait constant redeclared, not overridden
+--FILE--
+<?php
+
+trait DemoTrait {
+ public const T = 'T';
+}
+
+class UsesTrait {
+ use DemoTrait;
+
+ #[\Override]
+ public const T = 'T';
+}
+
+echo "Done";
+
+?>
+--EXPECTF--
+Fatal error: UsesTrait::T has #[\Override] attribute, but no matching parent constant exists in %s on line %d
diff --git a/Zend/tests/attributes/override/constants/trait_redeclared_interface.phpt b/Zend/tests/attributes/override/constants/trait_redeclared_interface.phpt
new file mode 100644
index 00000000000..21a1aa53224
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/trait_redeclared_interface.phpt
@@ -0,0 +1,25 @@
+--TEST--
+#[\Override]: Constants - trait constant redeclared, overrides interface
+--FILE--
+<?php
+
+interface IFace {
+ public const I = 'I';
+}
+
+trait DemoTrait {
+ public const I = 'I';
+}
+
+class UsesTrait implements IFace {
+ use DemoTrait;
+
+ #[\Override]
+ public const I = 'I';
+}
+
+echo "Done";
+
+?>
+--EXPECT--
+Done
diff --git a/Zend/tests/attributes/override/constants/trait_redeclared_parent.phpt b/Zend/tests/attributes/override/constants/trait_redeclared_parent.phpt
new file mode 100644
index 00000000000..c3d1e43d253
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/trait_redeclared_parent.phpt
@@ -0,0 +1,25 @@
+--TEST--
+#[\Override]: Constants - trait constant redeclared, overrides parent class
+--FILE--
+<?php
+
+class Base {
+ public const I = 'I';
+}
+
+trait DemoTrait {
+ public const I = 'I';
+}
+
+class UsesTrait extends Base {
+ use DemoTrait;
+
+ #[\Override]
+ public const I = 'I';
+}
+
+echo "Done";
+
+?>
+--EXPECT--
+Done
diff --git a/Zend/tests/attributes/override/constants/trait_unused.phpt b/Zend/tests/attributes/override/constants/trait_unused.phpt
new file mode 100644
index 00000000000..785f2888e90
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/trait_unused.phpt
@@ -0,0 +1,15 @@
+--TEST--
+#[\Override]: Constants - on a trait, unused
+--FILE--
+<?php
+
+trait Demo {
+ #[\Override]
+ public const T = 'T';
+}
+
+echo "Done";
+
+?>
+--EXPECT--
+Done
diff --git a/Zend/tests/attributes/override/constants/visibility_01.phpt b/Zend/tests/attributes/override/constants/visibility_01.phpt
new file mode 100644
index 00000000000..49671471f75
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/visibility_01.phpt
@@ -0,0 +1,19 @@
+--TEST--
+#[\Override]: Constants - private constant not overridden (by public constant)
+--FILE--
+<?php
+
+class Base {
+ private const C = 'C';
+}
+
+class Child extends Base {
+ #[\Override]
+ public const C = 'Changed';
+}
+
+echo "Done";
+
+?>
+--EXPECTF--
+Fatal error: Child::C has #[\Override] attribute, but no matching parent constant exists in %s on line %d
diff --git a/Zend/tests/attributes/override/constants/visibility_02.phpt b/Zend/tests/attributes/override/constants/visibility_02.phpt
new file mode 100644
index 00000000000..c160a29a618
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/visibility_02.phpt
@@ -0,0 +1,19 @@
+--TEST--
+#[\Override]: Constants - private constant not overridden (by private constant)
+--FILE--
+<?php
+
+class Base {
+ private const C = 'C';
+}
+
+class Child extends Base {
+ #[\Override]
+ private const C = 'Changed';
+}
+
+echo "Done";
+
+?>
+--EXPECTF--
+Fatal error: Child::C has #[\Override] attribute, but no matching parent constant exists in %s on line %d
diff --git a/Zend/tests/attributes/override/constants/visibility_03.phpt b/Zend/tests/attributes/override/constants/visibility_03.phpt
new file mode 100644
index 00000000000..db460f24375
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/visibility_03.phpt
@@ -0,0 +1,19 @@
+--TEST--
+#[\Override]: Constants - protected constant is overridden (by public constant)
+--FILE--
+<?php
+
+class Base {
+ protected const C = 'C';
+}
+
+class Child extends Base {
+ #[\Override]
+ public const C = 'Changed';
+}
+
+echo "Done";
+
+?>
+--EXPECT--
+Done
diff --git a/Zend/tests/attributes/override/constants/visibility_04.phpt b/Zend/tests/attributes/override/constants/visibility_04.phpt
new file mode 100644
index 00000000000..0ad46b022f4
--- /dev/null
+++ b/Zend/tests/attributes/override/constants/visibility_04.phpt
@@ -0,0 +1,19 @@
+--TEST--
+#[\Override]: Constants - protected constant is overridden (by protected constant)
+--FILE--
+<?php
+
+class Base {
+ protected const C = 'C';
+}
+
+class Child extends Base {
+ #[\Override]
+ protected const C = 'Changed';
+}
+
+echo "Done";
+
+?>
+--EXPECT--
+Done
diff --git a/Zend/zend_attributes.stub.php b/Zend/zend_attributes.stub.php
index ded9c89593a..05fd285d5b9 100644
--- a/Zend/zend_attributes.stub.php
+++ b/Zend/zend_attributes.stub.php
@@ -68,7 +68,7 @@ public function __debugInfo(): array {}
/**
* @strict-properties
*/
-#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_PROPERTY)]
+#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_PROPERTY|Attribute::TARGET_CLASS_CONSTANT)]
final class Override
{
public function __construct() {}
diff --git a/Zend/zend_attributes_arginfo.h b/Zend/zend_attributes_arginfo.h
index 54a66af2996..8acac398426 100644
Binary files a/Zend/zend_attributes_arginfo.h and b/Zend/zend_attributes_arginfo.h differ
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index 4c137521588..c75333e6d3c 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -9370,6 +9370,15 @@ static void zend_compile_class_const_decl(zend_ast *ast, uint32_t flags, zend_as
ce->ce_flags |= ZEND_ACC_HAS_AST_CONSTANTS;
ce->ce_flags &= ~ZEND_ACC_CONSTANTS_UPDATED;
}
+
+ const zend_attribute *override = zend_get_attribute_str(c->attributes, "override", sizeof("override") - 1);
+ if (override) {
+ ZEND_CLASS_CONST_FLAGS(c) |= ZEND_ACC_OVERRIDE;
+ /* We need to be able to remove the flag once the override is
+ * resolved. See ZEND_ACC_DEPRECATED above. */
+ ce->ce_flags |= ZEND_ACC_HAS_AST_CONSTANTS;
+ ce->ce_flags &= ~ZEND_ACC_CONSTANTS_UPDATED;
+ }
}
}
}
@@ -9829,6 +9838,16 @@ static void zend_compile_enum_case(zend_ast *ast)
if (deprecated) {
ZEND_CLASS_CONST_FLAGS(c) |= ZEND_ACC_DEPRECATED;
}
+
+ const zend_attribute *override = zend_get_attribute_str(c->attributes, "override", sizeof("override") - 1);
+ if (override) {
+ ZEND_CLASS_CONST_FLAGS(c) |= ZEND_ACC_OVERRIDE;
+ /* We need to be able to remove the flag once the override is
+ * resolved. See ZEND_ACC_DEPRECATED handling in
+ * zend_compile_class_const_decl(). */
+ enum_class->ce_flags |= ZEND_ACC_HAS_AST_CONSTANTS;
+ enum_class->ce_flags &= ~ZEND_ACC_CONSTANTS_UPDATED;
+ }
}
}
diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c
index 848b9d209b2..583b10d3418 100644
--- a/Zend/zend_inheritance.c
+++ b/Zend/zend_inheritance.c
@@ -2115,6 +2115,12 @@ static bool do_inherit_constant_check(
);
}
+ if (!(ZEND_CLASS_CONST_FLAGS(parent_constant) & ZEND_ACC_PRIVATE)) {
+ if (child_constant->ce == ce) {
+ ZEND_CLASS_CONST_FLAGS(child_constant) &= ~ZEND_ACC_OVERRIDE;
+ }
+ }
+
if (!(ZEND_CLASS_CONST_FLAGS(parent_constant) & ZEND_ACC_PRIVATE) && ZEND_TYPE_IS_SET(parent_constant->type)) {
inheritance_status status = class_constant_types_compatible(parent_constant, child_constant);
if (status == INHERITANCE_ERROR) {
@@ -2317,6 +2323,15 @@ void zend_inheritance_check_override(const zend_class_entry *ce)
}
} ZEND_HASH_FOREACH_END();
+ ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&ce->constants_table, zend_string *name, zend_class_constant *c) {
+ if (ZEND_CLASS_CONST_FLAGS(c) & ZEND_ACC_OVERRIDE) {
+ zend_error_noreturn(
+ E_COMPILE_ERROR,
+ "%s::%s has #[\\Override] attribute, but no matching parent constant exists",
+ ZSTR_VAL(ce->name), ZSTR_VAL(name));
+ }
+ } ZEND_HASH_FOREACH_END();
+
ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, zend_property_info *prop) {
if (prop->flags & ZEND_ACC_OVERRIDE) {
zend_error_noreturn(