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