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(