Commit 63e26ab7162 for php.net

commit 63e26ab71620a4f636bde1d6c657f9aca5ce987e
Author: Khaled Alam <khaledalam.net@gmail.com>
Date:   Tue Jul 21 01:32:23 2026 +0400

    Add trait support for internal classes (GH-20976)

    Fixes GH-20974

    Co-authored-by: Ilija Tovilo <ilija.tovilo@me.com>

diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS
index ae58e7ba8d5..ef5155e39db 100644
--- a/UPGRADING.INTERNALS
+++ b/UPGRADING.INTERNALS
@@ -146,6 +146,7 @@ PHP 8.6 INTERNALS UPGRADE NOTES
     php_stream_copy_to_stream_ex(). The mmap-based copy fallback was removed.
   . Added zend_string_equals_cstr_ci().
   . Added zend_string_ends_with() and related variants.
+  . Added trait support for internal classes.

 ========================
 2. Build system changes
diff --git a/Zend/zend_API.h b/Zend/zend_API.h
index bbfe6425774..2c0b892d194 100644
--- a/Zend/zend_API.h
+++ b/Zend/zend_API.h
@@ -395,6 +395,7 @@ ZEND_API zend_class_entry *zend_register_internal_class_ex(const zend_class_entr
 ZEND_API zend_class_entry *zend_register_internal_class_with_flags(const zend_class_entry *class_entry, zend_class_entry *parent_ce, uint32_t flags);
 ZEND_API zend_class_entry *zend_register_internal_interface(const zend_class_entry *orig_class_entry);
 ZEND_API void zend_class_implements(zend_class_entry *class_entry, int num_interfaces, ...);
+ZEND_API void zend_class_use_internal_traits(zend_class_entry *class_entry, int num_traits, ...);

 ZEND_API zend_result zend_register_class_alias_ex(const char *name, size_t name_len, zend_class_entry *ce, bool persistent);

diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c
index 583b10d3418..4424c9a1a3a 100644
--- a/Zend/zend_inheritance.c
+++ b/Zend/zend_inheritance.c
@@ -2403,7 +2403,11 @@ static void zend_add_trait_method(zend_class_entry *ce, zend_string *name, zend_
 		}
 	}

-	if (UNEXPECTED(fn->type == ZEND_INTERNAL_FUNCTION)) {
+	if (ce->type == ZEND_INTERNAL_CLASS) {
+		ZEND_ASSERT(fn->type == ZEND_INTERNAL_FUNCTION);
+		new_fn = (zend_function*)(uintptr_t)malloc(sizeof(zend_internal_function));
+		memcpy(new_fn, fn, sizeof(zend_internal_function));
+	} else if (UNEXPECTED(fn->type == ZEND_INTERNAL_FUNCTION)) {
 		new_fn = zend_arena_alloc(&CG(arena), sizeof(zend_internal_function));
 		memcpy(new_fn, fn, sizeof(zend_internal_function));
 		new_fn->common.fn_flags |= ZEND_ACC_ARENA_ALLOCATED;
@@ -2833,7 +2837,11 @@ static void zend_do_traits_constant_binding(zend_class_entry *ce, zend_class_ent
 			if (do_trait_constant_check(ce, constant, constant_name, traits, i)) {
 				zend_class_constant *ct = NULL;

-				ct = zend_arena_alloc(&CG(arena),sizeof(zend_class_constant));
+				if (ce->type == ZEND_INTERNAL_CLASS) {
+					ct = malloc(sizeof(zend_class_constant));
+				} else {
+					ct = zend_arena_alloc(&CG(arena),sizeof(zend_class_constant));
+				}
 				memcpy(ct, constant, sizeof(zend_class_constant));
 				constant = ct;

@@ -3012,6 +3020,49 @@ static void zend_do_traits_property_binding(zend_class_entry *ce, zend_class_ent
 }
 /* }}} */

+ZEND_API void zend_class_use_internal_traits(zend_class_entry *class_entry, int num_traits, ...)
+{
+	ZEND_ASSERT(class_entry->ce_flags & ZEND_ACC_LINKED);
+	ZEND_ASSERT(num_traits >= 0);
+
+	if (UNEXPECTED(num_traits == 0)) {
+		return;
+	}
+
+	zend_class_entry **traits = safe_pemalloc(num_traits, sizeof(zend_class_entry *), 0, /* persistent */ true);
+	class_entry->trait_names = safe_pemalloc(num_traits, sizeof(zend_class_name), 0, /* persistent */ true);
+	class_entry->num_traits = num_traits;
+
+	va_list trait_list;
+	va_start(trait_list, num_traits);
+	for (int i = 0; i < num_traits; i++) {
+		zend_class_entry *trait_entry = va_arg(trait_list, zend_class_entry *);
+		class_entry->trait_names[i].name = zend_string_copy(trait_entry->name);
+		class_entry->trait_names[i].lc_name = zend_string_tolower_ex(zend_string_copy(trait_entry->name), /* persistent */ true);
+
+		if (UNEXPECTED(!(trait_entry->ce_flags & ZEND_ACC_TRAIT))) {
+			free(traits);
+			zend_error_noreturn(E_COMPILE_ERROR, "Class %s cannot use %s - it is not a trait",
+				ZSTR_VAL(class_entry->name), ZSTR_VAL(trait_entry->name));
+		}
+		traits[i] = trait_entry;
+	}
+	va_end(trait_list);
+
+	bool contains_abstract_methods = false;
+	zend_do_traits_method_binding(class_entry, traits, NULL, NULL, false, &contains_abstract_methods);
+	zend_do_traits_constant_binding(class_entry, traits);
+	zend_do_traits_property_binding(class_entry, traits);
+
+	ZEND_HASH_MAP_FOREACH_PTR(&class_entry->function_table, zend_function *fn) {
+		zend_fixup_trait_method(fn, class_entry);
+	} ZEND_HASH_FOREACH_END();
+
+	free(traits);
+
+	/* TODO: Verify abstract trait method implementation requirements are enforced. */
+}
+
 #define MAX_ABSTRACT_INFO_CNT 3
 #define MAX_ABSTRACT_INFO_FMT "%s%s%s%s"
 #define DISPLAY_ABSTRACT_FN(idx) \
diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c
index 538eff3ea34..a25f236c3c2 100644
--- a/Zend/zend_opcode.c
+++ b/Zend/zend_opcode.c
@@ -485,7 +485,7 @@ ZEND_API void destroy_zend_class(zval *zv)
 			zend_string_release_ex(ce->name, 1);

 			ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, fn) {
-				if (fn->common.scope == ce) {
+				if (fn->common.scope == ce && !(fn->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) {
 					zend_free_internal_arg_info(&fn->internal_function, true);

 					if (fn->common.attributes) {
@@ -535,6 +535,13 @@ ZEND_API void destroy_zend_class(zval *zv)
 			if (ce->attributes) {
 				zend_hash_release(ce->attributes);
 			}
+			if (ce->num_traits > 0) {
+				for (uint32_t i = 0; i < ce->num_traits; i++) {
+					zend_string_release(ce->trait_names[i].name);
+					zend_string_release(ce->trait_names[i].lc_name);
+				}
+				free(ce->trait_names);
+			}
 			free(ce);
 			break;
 	}
diff --git a/build/gen_stub.php b/build/gen_stub.php
index 715a4718234..b2f38c978cf 100755
--- a/build/gen_stub.php
+++ b/build/gen_stub.php
@@ -13,6 +13,7 @@
 use PhpParser\Node\Stmt\Enum_;
 use PhpParser\Node\Stmt\Interface_;
 use PhpParser\Node\Stmt\Trait_;
+use PhpParser\Node\Stmt\TraitUse;
 use PhpParser\PrettyPrinter\Standard;
 use PhpParser\PrettyPrinterAbstract;

@@ -3383,6 +3384,7 @@ class ClassInfo {
      * @param AttributeInfo[] $attributes
      * @param Name[] $extends
      * @param Name[] $implements
+     * @param Name[] $uses
      * @param ConstInfo[] $constInfos
      * @param PropertyInfo[] $propertyInfos
      * @param FuncInfo[] $funcInfos
@@ -3401,6 +3403,7 @@ public function __construct(
         private bool $isNotSerializable,
         private readonly array $extends,
         private readonly array $implements,
+        private readonly array $uses,
         public /* readonly */ array $constInfos,
         private /* readonly */ array $propertyInfos,
         public array $funcInfos,
@@ -3421,6 +3424,9 @@ public function getRegistration(array $allConstInfos): string
         foreach ($this->implements as $implements) {
             $params[] = "zend_class_entry *class_entry_" . implode("_", $implements->getParts());
         }
+        foreach ($this->uses as $use) {
+            $params[] = "zend_class_entry *class_entry_" . implode("_", $use->getParts());
+        }

         $escapedName = implode("_", $this->name->getParts());

@@ -3518,6 +3524,17 @@ function (Name $item) {
             $code .= "\tzend_class_implements(class_entry, " . count($implements) . ", " . implode(", ", $implements) . ");\n";
         }

+        $traits = array_map(
+            function (Name $item) {
+                return "class_entry_" . implode("_", $item->getParts());
+            },
+            $this->uses
+        );
+
+        if (!empty($traits)) {
+            $code .= "\tzend_class_use_internal_traits(class_entry, " . count($traits) . ", " . implode(", ", $traits) . ");\n";
+        }
+
         if ($this->alias) {
             $code .= "\tzend_register_class_alias(\"" . str_replace("\\", "\\\\", $this->alias) . "\", class_entry);\n";
         }
@@ -4408,6 +4425,7 @@ private function handleStatements(array $stmts, PrettyPrinterAbstract $prettyPri
                 $propertyInfos = [];
                 $methodInfos = [];
                 $enumCaseInfos = [];
+                $traitUses = [];
                 foreach ($stmt->stmts as $classStmt) {
                     $cond = self::handlePreprocessorConditions($conds, $classStmt);
                     if ($classStmt instanceof Stmt\Nop) {
@@ -4469,6 +4487,13 @@ private function handleStatements(array $stmts, PrettyPrinterAbstract $prettyPri
                             $classStmt->expr,
                             $classStmt->expr ? $prettyPrinter->prettyPrintExpr($classStmt->expr) : null,
                         );
+                    } else if ($classStmt instanceof TraitUse) {
+                        if ($classStmt->adaptations) {
+                            throw new Exception("Trait adaptations are not supported");
+                        }
+                        foreach ($classStmt->traits as $trait) {
+                            $traitUses[] = $trait;
+                        }
                     } else {
                         throw new Exception("Not implemented {$classStmt->getType()}");
                     }
@@ -4481,6 +4506,7 @@ private function handleStatements(array $stmts, PrettyPrinterAbstract $prettyPri
                     $propertyInfos,
                     $methodInfos,
                     $enumCaseInfos,
+                    $traitUses,
                     $cond,
                     $this->getMinimumPhpVersionIdCompatibility(),
                     $this->isUndocumentable
@@ -5167,6 +5193,7 @@ function parseProperty(
  * @param PropertyInfo[] $properties
  * @param FuncInfo[] $methods
  * @param EnumCaseInfo[] $enumCases
+ * @param Name[] $traitUses
  */
 function parseClass(
     Name $name,
@@ -5175,6 +5202,7 @@ function parseClass(
     array $properties,
     array $methods,
     array $enumCases,
+    array $traitUses,
     ?string $cond,
     ?int $minimumPhpVersionIdCompatibility,
     bool $isUndocumentable
@@ -5247,6 +5275,7 @@ function parseClass(
         $isNotSerializable,
         $extends,
         $implements,
+        $traitUses,
         $consts,
         $properties,
         $methods,
diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c
index e8058936b6d..a880c09fc1f 100644
--- a/ext/zend_test/test.c
+++ b/ext/zend_test/test.c
@@ -54,6 +54,9 @@ static zend_class_entry *zend_test_child_class;
 static zend_class_entry *zend_test_gen_stub_flag_compatibility_test;
 static zend_class_entry *zend_attribute_test_class;
 static zend_class_entry *zend_test_trait;
+static zend_class_entry *zend_test_trait_for_internal_class;
+static zend_class_entry *zend_test_trait_for_internal_class2;
+static zend_class_entry *zend_test_class_with_traits;
 static zend_class_entry *zend_test_attribute;
 static zend_class_entry *zend_test_repeatable_attribute;
 static zend_class_entry *zend_test_parameter_attribute;
@@ -1305,6 +1308,18 @@ static ZEND_METHOD(_ZendTestTrait, testMethod)
 	RETURN_TRUE;
 }

+static ZEND_METHOD(_ZendTestTraitForInternalClass, traitMethod)
+{
+	ZEND_PARSE_PARAMETERS_NONE();
+	RETURN_LONG(789);
+}
+
+static ZEND_METHOD(_ZendTestTraitForInternalClass2, traitMethod2)
+{
+	ZEND_PARSE_PARAMETERS_NONE();
+	RETURN_LONG(101);
+}
+
 static ZEND_METHOD(ZendTestNS_Foo, method)
 {
 	ZEND_PARSE_PARAMETERS_NONE();
@@ -1607,6 +1622,9 @@ PHP_MINIT_FUNCTION(zend_test)
 	zend_attribute_test_class = register_class_ZendAttributeTest();

 	zend_test_trait = register_class__ZendTestTrait();
+	zend_test_trait_for_internal_class = register_class__ZendTestTraitForInternalClass();
+	zend_test_trait_for_internal_class2 = register_class__ZendTestTraitForInternalClass2();
+	zend_test_class_with_traits = register_class__ZendTestClassWithTraits(zend_test_trait_for_internal_class, zend_test_trait_for_internal_class2);

 	register_test_symbols(module_number);

diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php
index a4562368735..7c96ea176a8 100644
--- a/ext/zend_test/test.stub.php
+++ b/ext/zend_test/test.stub.php
@@ -35,6 +35,32 @@ interface _ZendTestInterface
         public const DUMMY = 0;
     }

+    trait _ZendTestTraitForInternalClass
+    {
+        /** @var int */
+        public const ZEND_TRAIT_CONST = 123;
+
+        public int $traitProp = 456;
+
+        public function traitMethod(): int {}
+    }
+
+    trait _ZendTestTraitForInternalClass2
+    {
+        /** @var int */
+        public const ZEND_TRAIT_CONST2 = 321;
+
+        public static int $staticTraitProp = 999;
+
+        public function traitMethod2(): int {}
+    }
+
+    class _ZendTestClassWithTraits
+    {
+        use _ZendTestTraitForInternalClass;
+        use _ZendTestTraitForInternalClass2;
+    }
+
     /** @alias _ZendTestClassAlias */
     class _ZendTestClass implements _ZendTestInterface {
         public const mixed TYPED_CLASS_CONST1 = [];
diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h
index bd6548e7bff..b2e342382db 100644
Binary files a/ext/zend_test/test_arginfo.h and b/ext/zend_test/test_arginfo.h differ
diff --git a/ext/zend_test/test_decl.h b/ext/zend_test/test_decl.h
index 2561000f4b6..ea6a2c94fbd 100644
Binary files a/ext/zend_test/test_decl.h and b/ext/zend_test/test_decl.h differ
diff --git a/ext/zend_test/test_legacy_arginfo.h b/ext/zend_test/test_legacy_arginfo.h
index a254a637e07..05014c80fd5 100644
Binary files a/ext/zend_test/test_legacy_arginfo.h and b/ext/zend_test/test_legacy_arginfo.h differ
diff --git a/ext/zend_test/tests/internal_class_traits.phpt b/ext/zend_test/tests/internal_class_traits.phpt
new file mode 100644
index 00000000000..c1a378ee99d
--- /dev/null
+++ b/ext/zend_test/tests/internal_class_traits.phpt
@@ -0,0 +1,47 @@
+--TEST--
+Test traits in internal classes
+--EXTENSIONS--
+zend_test
+--FILE--
+<?php
+
+// Constants from both traits
+var_dump(_ZendTestClassWithTraits::ZEND_TRAIT_CONST);
+var_dump(_ZendTestClassWithTraits::ZEND_TRAIT_CONST2);
+
+$obj = new _ZendTestClassWithTraits();
+
+// Property from trait 1
+var_dump($obj->traitProp);
+
+// Methods from both traits
+var_dump($obj->traitMethod());
+var_dump($obj->traitMethod2());
+
+// Static property from trait 2
+var_dump(_ZendTestClassWithTraits::$staticTraitProp);
+
+// Reflection should show both traits
+$rc = new ReflectionClass(_ZendTestClassWithTraits::class);
+$traits = $rc->getTraitNames();
+sort($traits);
+var_dump(count($traits));
+var_dump($traits);
+
+echo "Done\n";
+?>
+--EXPECT--
+int(123)
+int(321)
+int(456)
+int(789)
+int(101)
+int(999)
+int(2)
+array(2) {
+  [0]=>
+  string(30) "_ZendTestTraitForInternalClass"
+  [1]=>
+  string(31) "_ZendTestTraitForInternalClass2"
+}
+Done