重构 Trait 实现

pull/47/head
韩天峰 2 weeks ago
parent c4b8db030d
commit 833b74b344
  1. 7
      phpunit/src/ClassTest.php
  2. 2
      phpunit/src/InheritanceErrorTest.php
  3. 28
      phpunit/src/TraitFuncDeclTest.php
  4. 14
      src/CompilerBase.php
  5. 8
      src/Entity/ClassDef.php
  6. 2
      src/Entity/FunctionDef.php
  7. 10
      src/Entity/MethodDef.php
  8. 44
      src/Generator/AnonClassGenerator.php
  9. 6
      src/Generator/ClosureGenerator.php
  10. 14
      src/Optimizer/FuncCallOptimizer.php
  11. 4
      src/Optimizer/SsaTypeOptimizer.php
  12. 6
      src/Parser/AssignOpTrait.php
  13. 6
      src/Parser/ClassConstantFetchTrait.php
  14. 6
      src/Parser/ConstantExpressionTrait.php
  15. 27
      src/Parser/MethodCallTrait.php
  16. 21
      src/Preprocessor.php
  17. 458
      src/Translator.php
  18. 8
      src/gen_stub.php
  19. 2
      tests/compiler/stdlib/class-exists-class-constant.phpt
  20. 4
      tests/compiler/stdlib/class_exists.phpt
  21. 2
      tests/compiler/trait/002.phpt
  22. 52
      tests/compiler/trait/trait-ast-composition.phpt
  23. 55
      tests/compiler/trait/trait-import-context.phpt
  24. 4
      tests/std/core/002_class_exists.phpt

@ -640,9 +640,12 @@ class ClassTest extends \BaseTest
$this->exec('Cannot access private method `BaseSecret::secret()`', 'trait-parent-method-private.php'); $this->exec('Cannot access private method `BaseSecret::secret()`', 'trait-parent-method-private.php');
} }
public function testTraitMethodMayShadowPrivateParentMethod() public function testComposedTraitMethodCannotShadowPrivateParentMethod()
{ {
$this->compile('trait-method-shadows-private.php'); $this->exec(
'Cannot override private method `PrivateMethodParent::execute()`',
'trait-method-shadows-private.php'
);
} }
public function testSelfCanBePartOfUnionType() public function testSelfCanBePartOfUnionType()

@ -363,6 +363,6 @@ class InheritanceErrorTest extends TestCase
public function testTraitParentCallRequiresParentClass() public function testTraitParentCallRequiresParentClass()
{ {
$this->exec('has no parent', 'trait-parent-without-parent.php'); $this->exec('does not extend any class', 'trait-parent-without-parent.php');
} }
} }

@ -1,19 +1,9 @@
<?php <?php
/** /** Verifies that traits are AST templates rather than standalone native functions. */
* Regression test for the trait `parent::` / `trait_parent_ce` declaration bug.
*
* A trait method whose body contains `parent::` calls is compiled with an implicit
* `zend_class_entry *trait_parent_ce` parameter (so `parent::` can be bound to the
* class that composes the trait). The shared `func_decl.h` declaration must emit the
* same parameter; otherwise the generated C++ fails to compile with C2660
* ("function does not accept 3 arguments") at the call site that forwards to the trait
* function. This is a code-generation-level check that fails before the fix and passes
* after it.
*/
class TraitFuncDeclTest extends \BaseTest class TraitFuncDeclTest extends \BaseTest
{ {
public function testAliasedTraitConstructorParentCallDeclaresTraitParentCe(): void public function testAliasedTraitConstructorIsCompiledOnlyForComposingClasses(): void
{ {
// BaseTest::compile() populates the global $translator and translates the file. // BaseTest::compile() populates the global $translator and translates the file.
$this->compile('trait-aliased-constructor-parent-call.php'); $this->compile('trait-aliased-constructor-parent-call.php');
@ -30,16 +20,16 @@ class TraitFuncDeclTest extends \BaseTest
$compiler->genFunctionDeclarations($headerPath); $compiler->genFunctionDeclarations($headerPath);
$decl = file_get_contents($headerPath); $decl = file_get_contents($headerPath);
$this->assertMatchesRegularExpression( $this->assertDoesNotMatchRegularExpression(
'/extern void php_tpdodriver____construct\([^;\n]*trait_parent_ce[^;\n]*\);/', '/extern void php_tpdodriver____construct\(/',
$decl, $decl,
'The trait function declaration must include its implicit parent scope' 'A trait must not have a standalone native function declaration'
); );
$this->assertDoesNotMatchRegularExpression( $this->assertMatchesRegularExpression(
'/extern void php_(?:driver__tpdodriverconstruct|directdriver____construct)' '/extern void php_(?:driver__tpdodriverconstruct|directdriver____construct)\(/',
. '\([^;\n]*trait_parent_ce[^;\n]*\);/',
$decl, $decl,
'Composing-class wrapper declarations must not expose the implicit parent scope' 'Each composing class must receive its own native method declaration'
); );
$this->assertStringNotContainsString('trait_parent_ce', $decl);
} }
} }

@ -1051,16 +1051,29 @@ class CompilerBase implements PropertyAccessContext
protected function getFunctionName(FunctionLike $v): string protected function getFunctionName(FunctionLike $v): string
{ {
if ($this->methodDef !== null && $this->classDef !== null) {
return $this->getNativeName(
$this->parseIdentifier($v->name),
$this->classDef->namespace,
$this->classDef->name,
);
}
return $this->getNativeName($this->parseIdentifier($v->name), $this->namespace, $this->class); return $this->getNativeName($this->parseIdentifier($v->name), $this->namespace, $this->class);
} }
protected function getFullClassName(): string protected function getFullClassName(): string
{ {
if ($this->classDef !== null) {
return $this->classDef->getNamespacedName(false);
}
return ltrim($this->namespace . '\\' . $this->class, '\\'); return ltrim($this->namespace . '\\' . $this->class, '\\');
} }
protected function getFullClassLikeName(): string protected function getFullClassLikeName(): string
{ {
if ($this->classDef !== null) {
return $this->classDef->getNamespacedName(false);
}
$name = $this->class !== '' ? $this->class : $this->interface; $name = $this->class !== '' ? $this->class : $this->interface;
return ltrim($this->namespace . '\\' . $name, '\\'); return ltrim($this->namespace . '\\' . $name, '\\');
} }
@ -3254,6 +3267,7 @@ class CompilerBase implements PropertyAccessContext
$classDef->implements[$i] = new Node\Name\FullyQualified($ifaceName); $classDef->implements[$i] = new Node\Name\FullyQualified($ifaceName);
} }
} }
$this->flattenEmbeddedClassTraits($classDef);
// 将匿名类内部的类型引用(方法参数、返回值、属性等)转为全限定名称 // 将匿名类内部的类型引用(方法参数、返回值、属性等)转为全限定名称
$this->resolveAnonClassTypeNames($classDef); $this->resolveAnonClassTypeNames($classDef);
$this->context->beforeStmtLines[] = 'static THREAD_LOCAL bool ' . $className . '_defined = false;'; $this->context->beforeStmtLines[] = 'static THREAD_LOCAL bool ' . $className . '_defined = false;';

@ -66,6 +66,14 @@ class ClassDef extends ClassLikeDef
*/ */
public array $abstractMethodDefs = []; public array $abstractMethodDefs = [];
public ?Trait_ $trait = null; public ?Trait_ $trait = null;
/** @var list<string> */
public array $traitUseNamespaces = [];
/** @var array<string, string> */
public array $traitUseAliases = [];
/** @var array<string, string> */
public array $traitUseFunctions = [];
/** @var array<string, string> */
public array $traitUseConstants = [];
/** /**
* FullMethodName -> alias list * FullMethodName -> alias list

@ -33,8 +33,6 @@ class FunctionDef
public string $attributeFactoryScope = ''; public string $attributeFactoryScope = '';
/** External library imported by the stub containing this function. */ /** External library imported by the stub containing this function. */
public string $importLibrary = ''; public string $importLibrary = '';
/** Whether the native signature includes an implicit trait parent scope. */
public bool $hasTraitParentCeParameter = false;
public bool $returnTypeUndeclared = false; public bool $returnTypeUndeclared = false;
public bool $returnsByRef = false; public bool $returnsByRef = false;
public bool $generator = false; public bool $generator = false;

@ -22,14 +22,8 @@ class MethodDef
*/ */
public ?\PhpParser\Node\Stmt\ClassMethod $node = null; public ?\PhpParser\Node\Stmt\ClassMethod $node = null;
/** /** Source trait, retained only for diagnostics and the __TRAIT__ constant. */
* For methods defined inside a trait, records `parent::method()` calls so public string $traitOrigin = '';
* the compiler can validate their visibility against the parent of each
* class that uses the trait (the trait itself has no parent at compile time).
*
* @var array<int, array{method: string, node: \PhpParser\NodeAbstract}>
*/
public array $parentMethodCalls = [];
public function __construct(int $flags, string $name) public function __construct(int $flags, string $name)
{ {

@ -21,6 +21,7 @@ use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassConst; use PhpParser\Node\Stmt\ClassConst;
use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Property; use PhpParser\Node\Stmt\Property;
use PhpParser\Node\Stmt\TraitUse;
use PhpParser\NodeTraverser; use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract; use PhpParser\NodeVisitorAbstract;
use TypePhp\Resolver\Reflection; use TypePhp\Resolver\Reflection;
@ -32,6 +33,49 @@ trait AnonClassGenerator
return self::ANON_CLASS . $this->anonClassIndex++; return self::ANON_CLASS . $this->anonClassIndex++;
} }
/** Flatten trait templates before an anonymous class is evaluated by ZendVM. */
protected function flattenEmbeddedClassTraits(Class_ $class): void
{
$declaredMethods = [];
foreach ($class->stmts as $stmt) {
if ($stmt instanceof ClassMethod) {
$declaredMethods[strtolower($stmt->name->toString())] = true;
}
}
$injected = [];
foreach ($class->stmts as $stmt) {
if (!$stmt instanceof TraitUse) {
continue;
}
foreach ($stmt->traits as $traitName) {
$fullName = $this->getNamespacedClassName($traitName->toString());
if (!$this->hasClass($fullName)) {
$this->fatalError($stmt, "Trait `{$fullName}` not found");
}
$traitDef = $this->getClass($fullName);
$traitAst = clone $traitDef->trait;
foreach ($traitAst->stmts as $traitStmt) {
if ($traitStmt instanceof ClassMethod) {
$name = strtolower($traitStmt->name->toString());
if (isset($declaredMethods[$name])) {
continue;
}
$declaredMethods[$name] = true;
}
if (!$traitStmt instanceof TraitUse) {
$injected[] = $traitStmt;
}
}
}
}
$class->stmts = array_values(array_filter(
$class->stmts,
static fn (Node $stmt): bool => !$stmt instanceof TraitUse,
));
array_push($class->stmts, ...$injected);
}
/** /**
* Resolve all relative type names in an anonymous class to fully qualified names. * Resolve all relative type names in an anonymous class to fully qualified names.
* The generated eval code runs without use imports, so all type references must be FQN. * The generated eval code runs without use imports, so all type references must be FQN.

@ -302,9 +302,9 @@ trait ClosureGenerator
$this->indentLevel++; $this->indentLevel++;
try { try {
foreach ($capturedNames as $i => $name) { foreach ($capturedNames as $i => $capturedName) {
$code .= $this->getIndent() . Type::VAR . ' ' . $name . ' = vars_.get(' . $i . ');' . PHP_EOL; $code .= $this->getIndent() . Type::VAR . ' ' . $capturedName . ' = vars_.get(' . $i . ');' . PHP_EOL;
$this->addArgument($name, Type::VAR); $this->addArgument($capturedName, Type::VAR);
} }
if ($this->methodDef) { if ($this->methodDef) {
$this->addArgument('this_', Type::OBJECT); $this->addArgument('this_', Type::OBJECT);

@ -702,13 +702,21 @@ trait FuncCallOptimizer
return 'php::Decimal::round(' . $a0 . ')'; return 'php::Decimal::round(' . $a0 . ')';
} }
$args = count($e->args); $args = count($e->args);
// Keep PHP's left-to-right argument evaluation explicit here. This
// optimizer is itself compiled by TypePHP, and embedding several
// getArg() calls in one C++ string-concatenation expression would let
// the host C++ compiler choose a different evaluation order.
$a0 = $this->getArg($e, 0);
if ($args >= 3) { if ($args >= 3) {
return 'php::fn::round(' . $this->getArg($e, 0) . ', ' . $this->convertIntExpr($this->getArg($e, 1)) . ', ' . $this->convertIntExpr($this->getArg($e, 2)) . ')'; $a1 = $this->convertIntExpr($this->getArg($e, 1));
$a2 = $this->convertIntExpr($this->getArg($e, 2));
return 'php::fn::round(' . $a0 . ', ' . $a1 . ', ' . $a2 . ')';
} }
if ($args >= 2) { if ($args >= 2) {
return 'php::fn::round(' . $this->getArg($e, 0) . ', ' . $this->convertIntExpr($this->getArg($e, 1)) . ')'; $a1 = $this->convertIntExpr($this->getArg($e, 1));
return 'php::fn::round(' . $a0 . ', ' . $a1 . ')';
} }
return 'php::fn::round(' . $this->getArg($e, 0) . ')'; return 'php::fn::round(' . $a0 . ')';
} }
protected function genCount(string $n, Node\Expr\FuncCall $e, array $c): string protected function genCount(string $n, Node\Expr\FuncCall $e, array $c): string

@ -106,8 +106,8 @@ trait SsaTypeOptimizer
} }
} }
foreach ($groups as $varName => $varList) { foreach ($groups as $groupName => $varList) {
$varName = $this->escapeVarName($varName); $varName = $this->escapeVarName($groupName);
// Skip parameters — they already have declared types // Skip parameters — they already have declared types
if (isset($this->context->arguments[$varName])) { if (isset($this->context->arguments[$varName])) {
continue; continue;

@ -157,9 +157,9 @@ trait AssignOpTrait
$variables[] = $name; $variables[] = $name;
} }
foreach ($variables as $name) { foreach ($variables as $variableName) {
if (!$this->hasVar($name)) { if (!$this->hasVar($variableName)) {
$this->addLocalVar($name, Type::VAR); $this->addLocalVar($variableName, Type::VAR);
} }
} }
$tieItems = array_merge( $tieItems = array_merge(

@ -29,7 +29,11 @@ trait ClassConstantFetchTrait
$class = 'static'; $class = 'static';
} else { } else {
$self = true; $self = true;
$class = $this->class; // Trait-composed methods are parsed under the trait's lexical
// namespace, while `self` still denotes the consuming class.
// Keep it fully qualified so the lexical namespace is not
// applied to the class identity below.
$class = '\\' . $this->getFullClassName();
} }
} elseif ($class === 'parent') { } elseif ($class === 'parent') {
if (!$this->classDef || !$this->classDef->extends) { if (!$this->classDef || !$this->classDef->extends) {

@ -89,7 +89,8 @@ trait ConstantExpressionTrait
protected function parseMagicConst(MagicConst $expr): string protected function parseMagicConst(MagicConst $expr): string
{ {
$class = ($this->namespace ? $this->namespace . '\\' : '') . $this->class; $class = $this->classDef?->getNamespacedName(false)
?? (($this->namespace ? $this->namespace . '\\' : '') . $this->class);
$function = ($this->namespace ? $this->namespace . '\\' : '') . $this->function; $function = ($this->namespace ? $this->namespace . '\\' : '') . $this->function;
switch ($expr->getType()) { switch ($expr->getType()) {
case 'Scalar_MagicConst_Dir': case 'Scalar_MagicConst_Dir':
@ -109,6 +110,9 @@ trait ConstantExpressionTrait
} }
return '"' . $this->escapeString($class) . '"'; return '"' . $this->escapeString($class) . '"';
case 'Scalar_MagicConst_Trait': case 'Scalar_MagicConst_Trait':
if ($this->methodDef?->traitOrigin !== '') {
return '"' . $this->escapeString($this->methodDef->traitOrigin) . '"';
}
if (!$this->classDef or !$this->classDef->trait) { if (!$this->classDef or !$this->classDef->trait) {
$this->fatalError($expr, 'The magic constant `__TRAIT__` is not allowed in global scope'); $this->fatalError($expr, 'The magic constant `__TRAIT__` is not allowed in global scope');
} }

@ -243,33 +243,6 @@ trait MethodCallTrait
protected function parseParentMethodCall(Expr\StaticCall $expr): string protected function parseParentMethodCall(Expr\StaticCall $expr): string
{ {
// A trait's parent scope is supplied by the wrapper generated for the
// class that composes it. It must not be derived from the runtime
// object's class: an inherited trait method is still lexically bound to
// the parent of the composing class, not to the runtime object's parent.
if ($this->classDef !== null && $this->classDef->trait !== null) {
$method = $this->isIdExpr($expr->name) ? $this->parseIdentifier($expr->name) : '';
// Record the parent:: call so it can be validated against the parent
// of every class that uses this trait (the trait itself has no parent
// at compile time). Dynamic method names cannot be validated statically.
if ($method !== '' && isset($this->methodDef)) {
$this->methodDef->parentMethodCalls[] = ['method' => $method, 'node' => $expr];
}
$methodPtr = 'php::getMethod(trait_parent_ce, ' . $this->identifierToStr($expr->name) . ')';
if (empty($expr->args)) {
return 'this_.call(' . $methodPtr . ')';
}
// The concrete parent signature is only known at each trait use
// site. Preserve arguments that are already references so forwarding
// a by-reference trait parameter does not silently drop its alias.
return 'this_.call(' . $methodPtr . ', ' . $this->parseCallArgs(
$expr->args,
$method,
'',
preserveExistingReferences: true
) . ')';
}
if (!$this->classDef->extends) { if (!$this->classDef->extends) {
$this->fatalError($expr, 'Cannot call parent method because class `' . $this->classDef->name . '` does not extend any class'); $this->fatalError($expr, 'Cannot call parent method because class `' . $this->classDef->name . '` does not extend any class');
} }

@ -796,6 +796,13 @@ class Preprocessor extends CompilerBase
$this->classDef->implements = $this->parseImplements($class->implements); $this->classDef->implements = $this->parseImplements($class->implements);
} else { } else {
$this->classDef->trait = $class; $this->classDef->trait = $class;
// Trait members are compiled later in the consuming class, but
// names inside them retain the lexical imports of the trait
// declaration.
$this->classDef->traitUseNamespaces = $this->useNamespaces;
$this->classDef->traitUseAliases = $this->useAliases;
$this->classDef->traitUseFunctions = $this->useFunctions;
$this->classDef->traitUseConstants = $this->useConstants;
} }
$this->symbolDeclInFile[$fullClassNameLower] = $this->file; $this->symbolDeclInFile[$fullClassNameLower] = $this->file;
@ -1463,6 +1470,10 @@ class Preprocessor extends CompilerBase
if (!$abstract) { if (!$abstract) {
$this->methodDef = new MethodDef($flags, $name); $this->methodDef = new MethodDef($flags, $name);
$this->methodDef->node = $v; $this->methodDef->node = $v;
$traitOrigin = $v->getAttribute('typephp_trait_origin');
if (is_string($traitOrigin)) {
$this->methodDef->traitOrigin = $traitOrigin;
}
if ($this->classDef->hasMethod($name)) { if ($this->classDef->hasMethod($name)) {
$generatedBy = $v->getAttribute(CompileTimeAttributeDiagnostic::GENERATED_BY); $generatedBy = $v->getAttribute(CompileTimeAttributeDiagnostic::GENERATED_BY);
$generatedTarget = $v->getAttribute(CompileTimeAttributeDiagnostic::GENERATED_TARGET); $generatedTarget = $v->getAttribute(CompileTimeAttributeDiagnostic::GENERATED_TARGET);
@ -1479,6 +1490,12 @@ class Preprocessor extends CompilerBase
$this->fatalError($v, "Duplicate method `{$this->method}`"); $this->fatalError($v, "Duplicate method `{$this->method}`");
} }
$this->prepareFunction($v); $this->prepareFunction($v);
if ($class instanceof Node\Stmt\Trait_) {
// Trait functions are templates, not native symbols. The
// FunctionDef remains owned by MethodDef and is cloned into
// every class that composes the trait.
$this->symbols->removeFunction($this->getFunctionName($v));
}
$this->checkRequiredArgNum($name, $this->methodDef, $v); $this->checkRequiredArgNum($name, $this->methodDef, $v);
$this->classDef->addMethod($this->methodDef); $this->classDef->addMethod($this->methodDef);
} else { } else {
@ -1490,6 +1507,10 @@ class Preprocessor extends CompilerBase
} }
$this->methodDef = new MethodDef($flags, $name); $this->methodDef = new MethodDef($flags, $name);
$this->methodDef->node = $v; $this->methodDef->node = $v;
$traitOrigin = $v->getAttribute('typephp_trait_origin');
if (is_string($traitOrigin)) {
$this->methodDef->traitOrigin = $traitOrigin;
}
$this->methodDef->functionDef = $this->parseFunctionDecl($v); $this->methodDef->functionDef = $this->parseFunctionDecl($v);
$this->methodDef->functionDef->method = true; $this->methodDef->functionDef->method = true;
$this->checkRequiredArgNum($name, $this->methodDef, $v); $this->checkRequiredArgNum($name, $this->methodDef, $v);

@ -51,9 +51,12 @@ use PhpParser\Node;
use PhpParser\NodeAbstract; use PhpParser\NodeAbstract;
use PhpParser\NodeTraverser; use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver; use PhpParser\NodeVisitor\NameResolver;
use PhpParser\NodeVisitor\CloningVisitor;
class Translator extends Preprocessor class Translator extends Preprocessor
{ {
private const string TRAIT_ORIGIN_ATTRIBUTE = 'typephp_trait_origin';
private const string TRAIT_METHOD_ATTRIBUTE = 'typephp_trait_method';
use DefaultArgumentGenerator; use DefaultArgumentGenerator;
use NativeCommandOptionsTrait; use NativeCommandOptionsTrait;
use SourcePipelineTrait; use SourcePipelineTrait;
@ -1651,9 +1654,6 @@ CODE;
$list = []; $list = [];
if ($func->method) { if ($func->method) {
$list[] = Type::OBJECT . ' &this_'; $list[] = Type::OBJECT . ' &this_';
if ($func->hasTraitParentCeParameter) {
$list[] = 'zend_class_entry *trait_parent_ce';
}
} }
$argInfoList = $func->argInfoList; $argInfoList = $func->argInfoList;
if ($argInfoList) { if ($argInfoList) {
@ -2011,7 +2011,11 @@ CODE;
*/ */
private function getClassLikesWithConstants(): array private function getClassLikesWithConstants(): array
{ {
return array_merge($this->symbols->classes(), $this->symbols->interfaces()); $classes = array_filter(
$this->symbols->classes(),
static fn (ClassDef $classDef): bool => $classDef->trait === null,
);
return array_merge($classes, $this->symbols->interfaces());
} }
protected function getFilesFromDir(string $path): array protected function getFilesFromDir(string $path): array
@ -2509,6 +2513,9 @@ CODE;
} }
foreach ($this->symbols->classes() as $classDef) { foreach ($this->symbols->classes() as $classDef) {
if ($classDef->trait !== null) {
continue;
}
$ce = $this->getClassCe($classDef); $ce = $this->getClassCe($classDef);
$deps = []; $deps = [];
$parent = $classDef->extends; $parent = $classDef->extends;
@ -2542,7 +2549,13 @@ CODE;
$sorter->add($ce, $deps); $sorter->add($ce, $deps);
} }
$this->classCeList = $sorter->sort(); // StringSort yields an empty placeholder when the symbol table contains
// only compile-time traits. Never turn that placeholder into a bogus
// `zend_class_entry *;` declaration.
$this->classCeList = array_values(array_filter(
$sorter->sort(),
static fn (mixed $ce): bool => is_string($ce) && $ce !== '',
));
} }
protected function getNativeMethodName(ClassDef $classDef, MethodDef $methodDef): string protected function getNativeMethodName(ClassDef $classDef, MethodDef $methodDef): string
@ -2645,7 +2658,7 @@ CODE;
$this->argInfoHeaderFiles[] = $headerFile; $this->argInfoHeaderFiles[] = $headerFile;
} }
public function parseTraitUseForStub(Node\Stmt\ClassLike $stmt, Node\Name $className): void public function composeTraitAst(Node\Stmt\ClassLike $stmt, Node\Name $className): void
{ {
$methods = []; $methods = [];
$constants = []; $constants = [];
@ -2690,12 +2703,20 @@ CODE;
$this->fatalError($classStmt, "Trait `{$traitFullName}` not found"); $this->fatalError($classStmt, "Trait `{$traitFullName}` not found");
} }
$traitAst = clone $traitDef->trait; /** @var Node\Stmt\Trait_ $traitAst */
$traitAst = $this->cloneAstNode($traitDef->trait);
// Recursively flatten traits used by this trait before copying
// its members into the final class.
$this->composeTraitAst($traitAst, new Node\Name($traitFullName));
$traitStmts = $traitAst->stmts; $traitStmts = $traitAst->stmts;
$aliasStmts = []; $aliasStmts = [];
foreach ($traitStmts as $k1 => $traitStmt) { foreach ($traitStmts as $k1 => $traitStmt) {
if ($traitStmt instanceof Node\Stmt\ClassMethod) { if ($traitStmt instanceof Node\Stmt\ClassMethod) {
$methodName = strtolower($traitStmt->name->toString()); $methodName = strtolower($traitStmt->name->toString());
if ($traitStmt->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE) === null) {
$traitStmt->setAttribute(self::TRAIT_ORIGIN_ATTRIBUTE, $traitFullName);
$traitStmt->setAttribute(self::TRAIT_METHOD_ATTRIBUTE, $traitStmt->name->toString());
}
$fullMethodName = $this->getFullMethodName($traitFullName, $methodName); $fullMethodName = $this->getFullMethodName($traitFullName, $methodName);
// A trait method's `self`/`static`/`parent` return and parameter // A trait method's `self`/`static`/`parent` return and parameter
// types refer to the class that uses the trait, not the trait // types refer to the class that uses the trait, not the trait
@ -2820,6 +2841,14 @@ CODE;
$stmt->stmts = array_merge($stmt->stmts, $traitStmts, $aliasStmts); $stmt->stmts = array_merge($stmt->stmts, $traitStmts, $aliasStmts);
} }
} }
}
private function cloneAstNode(Node $node): Node
{
$traverser = new NodeTraverser();
$traverser->addVisitor(new CloningVisitor());
return $traverser->traverse([$node])[0];
} }
/** /**
@ -3177,6 +3206,15 @@ CODE;
} }
} }
if ($class instanceof Node\Stmt\Class_ || $class instanceof Node\Stmt\Enum_) {
/** @var Node\Stmt\Class_|Node\Stmt\Enum_ $composedClass */
$composedClass = $this->cloneAstNode($class);
$this->composeTraitAst($composedClass, new Node\Name($fullName));
$this->installComposedTraitDataMembers($composedClass);
} else {
$composedClass = null;
}
$this->checkPropertyOverride($class); $this->checkPropertyOverride($class);
$this->checkConstantOverride($class); $this->checkConstantOverride($class);
@ -3194,7 +3232,9 @@ CODE;
case 'Stmt_EnumCase': case 'Stmt_EnumCase':
break; break;
case 'Stmt_ClassMethod': case 'Stmt_ClassMethod':
if (!$class instanceof Node\Stmt\Trait_) {
$this->parseClassMethod($v, $methodCodes); $this->parseClassMethod($v, $methodCodes);
}
break; break;
case 'Stmt_TraitUse': case 'Stmt_TraitUse':
$this->parseTraitUse($v, $methodCodes); $this->parseTraitUse($v, $methodCodes);
@ -3204,6 +3244,29 @@ CODE;
break; break;
} }
} }
if ($composedClass !== null) {
$composedTraitMethods = [];
foreach ($composedClass->stmts as $stmt) {
if (!$stmt instanceof Node\Stmt\ClassMethod
|| !is_string($stmt->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE))) {
continue;
}
$origin = (string) $stmt->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE);
$this->withTraitNameContext($origin, function () use ($stmt): void {
$this->installComposedTraitMethod($stmt);
});
$composedTraitMethods[] = [$stmt, $origin];
}
// Every method must be visible before any body is lowered. Trait
// methods may call a private helper declared later in the same
// trait; compiling as we install would incorrectly lower that call
// as a dynamic callback instead of a native class method call.
foreach ($composedTraitMethods as [$stmt, $origin]) {
$this->withTraitNameContext($origin, function () use ($stmt, &$methodCodes): void {
$this->parseClassMethod($stmt, $methodCodes);
});
}
}
if (!$class instanceof Node\Stmt\Trait_) { if (!$class instanceof Node\Stmt\Trait_) {
$this->validateOverrideAttributes($class); $this->validateOverrideAttributes($class);
$this->checkInterfaceImplementations($class); $this->checkInterfaceImplementations($class);
@ -3225,9 +3288,8 @@ CODE;
protected function genNativeMethod(array $methodCodes): string protected function genNativeMethod(array $methodCodes): string
{ {
$code = ''; $code = '';
$classDef = $this->classDef; foreach ($methodCodes as $methodCode) {
foreach ($classDef->methods as $method) { $code .= $methodCode . PHP_EOL;
$code .= $methodCodes[$method->name] . PHP_EOL;
} }
$code .= PHP_EOL; $code .= PHP_EOL;
@ -3394,17 +3456,10 @@ CODE;
$cppCode = 'ZEND_METHOD(' . $name . ', ' . $methodDef->name . '){' . PHP_EOL; $cppCode = 'ZEND_METHOD(' . $name . ', ' . $methodDef->name . '){' . PHP_EOL;
$cppCode .= $this->getIndent() . Type::OBJECT . ' this_(&execute_data->This);' . PHP_EOL; $cppCode .= $this->getIndent() . Type::OBJECT . ' this_(&execute_data->This);' . PHP_EOL;
$fn = self::PREFIX . $this->getNativeMethodName($classDef, $methodDef); $fn = self::PREFIX . $this->getNativeMethodName($classDef, $methodDef);
$implicitMethodArgs = [];
if ($classDef->trait !== null && $methodDef->parentMethodCalls) {
// Trait methods are not directly callable without a composing class,
// but keep the generated Zend wrapper well-formed.
$implicitMethodArgs[] = 'this_.parent_ce()';
}
$cppCode .= $this->genWrapperFunctionArgs( $cppCode .= $this->genWrapperFunctionArgs(
$fn, $fn,
$methodDef->functionDef, $methodDef->functionDef,
$classDef->getNamespacedName(false) . '::' . $methodDef->name, $classDef->getNamespacedName(false) . '::' . $methodDef->name,
$implicitMethodArgs
); );
return $cppCode; return $cppCode;
@ -3428,7 +3483,7 @@ CODE;
$cppCode = ''; $cppCode = '';
// 接口没有方法实体 // 接口没有方法实体
if ($classDef instanceof ClassDef) { if ($classDef instanceof ClassDef && $classDef->trait === null) {
$defaultPropCount = 0; $defaultPropCount = 0;
foreach ($classDef->properties as $property) { foreach ($classDef->properties as $property) {
if (!$property->isStatic() && $property->default !== null) { if (!$property->isStatic() && $property->default !== null) {
@ -3598,16 +3653,11 @@ CODE;
$cppReturnType = $multiReturn $cppReturnType = $multiReturn
? $this->functionDef->getMultiReturnCppType() ? $this->functionDef->getMultiReturnCppType()
: ($this->functionDef->returnsByRef ? Type::REF : $this->getReturnType()); : ($this->functionDef->returnsByRef ? Type::REF : $this->getReturnType());
$this->functionDef->hasTraitParentCeParameter =
$this->classDef?->trait !== null && (bool) $this->methodDef?->parentMethodCalls;
$nativeName = self::PREFIX . $name; $nativeName = self::PREFIX . $name;
$functionAttribute = $this->getFunctionOptimizationAttribute($this->functionDef); $functionAttribute = $this->getFunctionOptimizationAttribute($this->functionDef);
$functionDeclCode = $functionAttribute . $cppReturnType . ' ' . ($multiReturn ? $this->getMultiReturnImplName($name) : $nativeName) . '('; $functionDeclCode = $functionAttribute . $cppReturnType . ' ' . ($multiReturn ? $this->getMultiReturnImplName($name) : $nativeName) . '(';
if ($this->class) { if ($this->class) {
$functionDeclCode .= Type::OBJECT . ' &this_'; $functionDeclCode .= Type::OBJECT . ' &this_';
if ($this->functionDef->hasTraitParentCeParameter) {
$functionDeclCode .= ', zend_class_entry *trait_parent_ce';
}
if ($this->functionDef->params) { if ($this->functionDef->params) {
$functionDeclCode .= ', '; $functionDeclCode .= ', ';
} }
@ -4504,7 +4554,6 @@ CODE;
protected function parseTraitUse(Node\Stmt\TraitUse $v, array &$methodCodes): void protected function parseTraitUse(Node\Stmt\TraitUse $v, array &$methodCodes): void
{ {
$classDef = $this->classDef; $classDef = $this->classDef;
foreach ($v->traits as $trait) { foreach ($v->traits as $trait) {
$traitName = $this->parseIdentifier($trait); $traitName = $this->parseIdentifier($trait);
$traitFullName = $this->getNamespacedClassName($traitName); $traitFullName = $this->getNamespacedClassName($traitName);
@ -4512,7 +4561,6 @@ CODE;
$this->fatalError($v, $traitFullName . ' not found'); $this->fatalError($v, $traitFullName . ' not found');
} }
$traitDef = $this->getClass($traitFullName); $traitDef = $this->getClass($traitFullName);
// 将 Trait 中定义的 常量、静态常量、属性、方法、静态属性复制到当前类中
foreach ($traitDef->constants as $const) { foreach ($traitDef->constants as $const) {
if ($classDef->hasConstant($const->name)) { if ($classDef->hasConstant($const->name)) {
if (!$this->isCompatibleTraitConstant($classDef->getConstant($const->name), $const)) { if (!$this->isCompatibleTraitConstant($classDef->getConstant($const->name), $const)) {
@ -4531,196 +4579,104 @@ CODE;
} }
$classDef->properties[$prop->name] = $prop; $classDef->properties[$prop->name] = $prop;
} }
foreach ($traitDef->methods as $methodDef) {
$classMethodName = $traitMethodName = $methodDef->name;
$fullMethodName = $this->getFullMethodName($traitFullName, $traitMethodName);
$originalMethodDef = $methodDef;
$aliasMethodDefs = [];
foreach ($classDef->traitAliases[$fullMethodName] ?? [] as $alias) {
if (strtolower($alias['newName']) === strtolower($traitMethodName)) {
if ($alias['newModifier']) {
if ($originalMethodDef === $methodDef) {
$originalMethodDef = clone $methodDef;
} }
$originalMethodDef->flags = $this->parseModifiers($alias['newModifier']);
} }
continue;
private function installComposedTraitDataMembers(Node\Stmt\ClassLike $class): void
{
foreach ($class->stmts as $stmt) {
if ($stmt instanceof Node\Stmt\ClassConst) {
foreach ($stmt->consts as $const) {
if (!$this->classDef->hasConstant($const->name->toString())) {
$this->parseClassConstDef($stmt);
break;
} }
$aliasMethodDef = clone $methodDef;
$classMethodName = $aliasMethodDef->name = $alias['newName'];
if ($alias['newModifier']) {
$aliasMethodDef->flags = $this->parseModifiers($alias['newModifier']);
} }
$aliasMethodDefs[strtolower($classMethodName)] = [$classMethodName, $aliasMethodDef]; } elseif ($stmt instanceof Node\Stmt\Property) {
foreach ($stmt->props as $prop) {
if (!$this->classDef->hasProperty($prop->name->toString())) {
$this->parseClassPropertyDef($stmt);
break;
} }
// 设置了 insteadof 选项,此 Trait 的方法将不会被使用
if (isset($classDef->traitIgnored[$fullMethodName])) {
foreach ($aliasMethodDefs as [$aliasMethodName, $aliasMethodDef]) {
if ($classDef->hasMethod($aliasMethodName)) {
continue;
} }
$methodCodes[$aliasMethodName] = $this->addTraitMethodWrapper(
$classDef,
$traitDef,
$aliasMethodDef,
$traitMethodName,
$aliasMethodName
);
} }
continue;
} }
// 类中已经有同名方法,则不使用 Trait 中的方法
if (!$classDef->hasMethod($traitMethodName)) {
$methodCodes[$traitMethodName] = $this->addTraitMethodWrapper(
$classDef,
$traitDef,
$originalMethodDef,
$traitMethodName,
$traitMethodName
);
} }
foreach ($aliasMethodDefs as [$aliasMethodName, $aliasMethodDef]) { private function installComposedTraitMethod(Node\Stmt\ClassMethod $methodStmt): void
if ($classDef->hasMethod($aliasMethodName)) { {
continue; $name = $methodStmt->name->toString();
} if ($this->classDef->hasMethod($name)) {
$methodCodes[$aliasMethodName] = $this->addTraitMethodWrapper( return;
$classDef,
$traitDef,
$aliasMethodDef,
$traitMethodName,
$aliasMethodName
);
}
} }
$flags = $this->parseModifiers($methodStmt->flags);
$methodDef = new MethodDef($flags, $name);
$methodDef->node = $methodStmt;
$methodDef->traitOrigin = (string) $methodStmt->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE, '');
$this->method = $name;
$this->methodDef = $methodDef;
if ($flags & Modifiers::ABSTRACT) {
$methodDef->functionDef = $this->parseFunctionDecl($methodStmt);
$methodDef->functionDef->method = true;
$this->checkRequiredArgNum($name, $methodDef, $methodStmt);
$this->classDef->addAbstractMethod($name, $flags, $methodDef);
} else {
$this->prepareFunction($methodStmt);
$this->checkRequiredArgNum($name, $methodDef, $methodStmt);
$this->classDef->addMethod($methodDef);
} }
$this->resetMethod();
} }
private function addTraitMethodWrapper( private function withTraitNameContext(string $traitName, callable $callback): mixed
ClassDef $classDef, {
ClassDef $traitDef, if (!$this->hasClass($traitName)) {
MethodDef $methodDef, $this->error("Internal compiler error: trait `{$traitName}` is not available while composing AST");
string $traitMethodName,
string $classMethodName
): string {
// A trait may be composed by multiple classes and aliases. Each wrapper
// needs independent signature metadata.
$methodDef = clone $methodDef;
// A trait method's `self`/`static`/`parent` return and parameter types
// refer to the class that uses the trait, not the trait itself. Re-resolve
// them to the consuming class so signature-compatibility checks (against
// parent classes and interfaces) and `detectClassOfExpr()` observe the
// correct type. The cloned FunctionDef keeps the trait's own native
// function untouched.
$this->reresolveTraitLateBoundTypes($classDef, $methodDef);
// The wrapper computes the composing class's parent scope and passes it
// to the trait function; it does not expose that scope in its signature.
$methodDef->functionDef->hasTraitParentCeParameter = false;
// Validate `parent::` calls emitted from this trait method against the
// parent of the class that is composing the trait. The trait itself has
// no parent at compile time, so this is the only place the parent class
// (and therefore the visibility of its methods) is known.
foreach ($methodDef->parentMethodCalls as $parentCall) {
$this->validateTraitParentCall($traitDef, $classDef, $parentCall['method'], $parentCall['node']);
}
// A trait method flattened into a class participates in the inheritance
// hierarchy: it must remain signature-compatible with any same-named
// parent method, exactly as a directly-declared override would. PHP
// enforces this at class declaration time ("Declaration of X::m() must
// be compatible with Y::m()"); without this check the incompatibility
// only surfaces as a runtime fatal error that the compiled binary would
// otherwise ignore and keep executing past.
$this->checkTraitMethodOverrideCompatibility($classDef, $methodDef, $classMethodName);
$classDef->addMethod($methodDef);
$traitMethodNativeName = $this->getNativeName($traitMethodName, $traitDef->namespace, $traitDef->name);
$classMethodNativeName = $this->getNativeName($classMethodName, $classDef->namespace, $classDef->name);
$argList = ['this_'];
if ($methodDef->parentMethodCalls) {
// Bind parent:: to the class that actually composes the trait. This
// remains correct when the generated wrapper is inherited further.
$argList[] = $this->getClassEntryPtr($classDef->extends);
}
foreach ($methodDef->functionDef->argInfoList as $argInfo) {
$argList[] = $argInfo->name;
}
$argv = implode(', ', $argList);
$cppReturnType = $methodDef->functionDef->returnsByRef ? Type::REF : $methodDef->getReturnType();
$code = $cppReturnType . ' ' . self::PREFIX . $classMethodNativeName . '(';
if ($this->class) {
$code .= Type::OBJECT . ' &this_';
if ($methodDef->functionDef->params) {
$code .= ', ';
} }
$traitDef = $this->getClass($traitName);
if ($traitDef->trait === null) {
$this->error("Internal compiler error: `{$traitName}` is not a trait AST template");
} }
$this->addFunction($classMethodNativeName, $methodDef->functionDef); $savedNamespace = $this->namespace;
$savedUseNamespaces = $this->useNamespaces;
$savedUseAliases = $this->useAliases;
$savedUseFunctions = $this->useFunctions;
$savedUseConstants = $this->useConstants;
$code .= $methodDef->functionDef->params . ')'; $this->namespace = $traitDef->namespace;
$code .= '{' . PHP_EOL; $this->useNamespaces = $traitDef->traitUseNamespaces;
$this->indentLevel++; $this->useAliases = $traitDef->traitUseAliases;
// The trait body is compiled once, but its private/protected property $this->useFunctions = $traitDef->traitUseFunctions;
// scope is the class that composes it. Keep that lexical scope fixed $this->useConstants = $traitDef->traitUseConstants;
// when this wrapper is inherited by a child class. try {
$scope = $this->getClassEntryPtr($classDef->getNamespacedName(false)); return $callback();
$code .= $this->getIndent() . 'php::FakeScopeGuard fake_scope_guard{' . $scope . '};' . PHP_EOL; } finally {
$methodCall = self::PREFIX . $traitMethodNativeName . '(' . $argv . ')'; $this->namespace = $savedNamespace;
if ($cppReturnType !== Type::VOID) { $this->useNamespaces = $savedUseNamespaces;
$methodCall = 'return ' . $methodCall; $this->useAliases = $savedUseAliases;
} $this->useFunctions = $savedUseFunctions;
$code .= $this->getIndent() . $methodCall . ';' . PHP_EOL; $this->useConstants = $savedUseConstants;
$this->indentLevel--; }
$code .= $this->getIndent() . '}' . PHP_EOL;
return $code;
} }
/** private function isCompatibleTraitConstant(ConstantDef $existing, ConstantDef $incoming): bool
* Re-resolve a trait method's late-bound `self`/`static`/`parent` return and
* parameter types to the class that is composing the trait.
*
* In PHP, `self` (and `static`) inside a trait refers to the using class, and
* `parent` refers to the using class's parent. The compiler records these as
* the trait's own name at parse time, which is wrong once the method is
* flattened into a class: interface/trait `self` comparisons and
* `detectClassOfExpr()` would otherwise observe the trait name instead of the
* consuming class. We clone the FunctionDef so the trait's standalone native
* function keeps its original (trait-context) types.
*/
private function reresolveTraitLateBoundTypes(ClassDef $usingClassDef, MethodDef $methodDef): void
{ {
$fn = $methodDef->functionDef; return $existing->flags === $incoming->flags
// Always produce a distinct FunctionDef for the composing-class wrapper. && $existing->type === $incoming->type
// The wrapper has a separate native signature from the trait function. && $existing->class === $incoming->class
$newFn = clone $fn; && $existing->value === $incoming->value;
if ($fn->returnTypeKeyword !== '') {
$resolved = $this->resolveLateBoundClass($usingClassDef, $fn->returnTypeKeyword);
if ($resolved !== null && $resolved !== $newFn->returnClass) {
$newFn->returnClass = $resolved;
}
} }
$newArgs = [];
foreach ($newFn->argInfoList as $arg) { private function isCompatibleTraitProperty(PropertyDef $existing, PropertyDef $incoming): bool
$newArg = clone $arg; {
if ($newArg->typeKeyword !== '') { return $existing->flags === $incoming->flags
$resolved = $this->resolveLateBoundClass($usingClassDef, $newArg->typeKeyword); && $existing->type === $incoming->type
if ($resolved !== null) { && $existing->class === $incoming->class
if ($newArg->class !== '') { && $existing->nullable === $incoming->nullable
$newArg->class = $resolved; && $existing->default === $incoming->default;
}
if ($newArg->declaredClass !== '') {
$newArg->declaredClass = $resolved;
}
}
}
$newArgs[] = $newArg;
}
$newFn->argInfoList = $newArgs;
$methodDef->functionDef = $newFn;
} }
private function resolveLateBoundClass(ClassDef $usingClassDef, string $keyword): ?string private function resolveLateBoundClass(ClassDef $usingClassDef, string $keyword): ?string
@ -4739,122 +4695,6 @@ CODE;
return null; return null;
} }
/**
* Validate a `parent::method()` call recorded inside a trait method.
*
* The trait has no parent of its own, so the only point at which the parent
* class is known is when a class actually uses the trait. At that moment we
* can statically resolve the parent method and reject private methods, which
* PHP would otherwise only report as a runtime "Call to private method" error.
*/
private function validateTraitParentCall(ClassDef $traitDef, ClassDef $usingClassDef, string $method, NodeAbstract $node): void
{
if (!$usingClassDef->extends) {
$this->fatalError(
$node,
"Cannot access parent when class `{$usingClassDef->getNamespacedName(false)}` has no parent"
);
}
$parentClass = $usingClassDef->extends;
// Internal / not-compiled parents are opaque to the compiler; let the
// runtime enforce visibility for those.
if (!$this->hasClass($parentClass)) {
return;
}
if ($this->getMethodFlags($parentClass, $method) & Modifiers::PRIVATE) {
$this->fatalError(
$node,
"Cannot access private method `{$parentClass}::{$method}()` via parent:: in trait `{$traitDef->name}`"
);
}
}
/**
* Validate that a trait method being flattened into a class remains
* signature-compatible with any same-named method declared up the parent
* chain — the same compatibility contract a directly-declared override must
* satisfy (see `checkParentMethodCanBeOverridden`).
*
* Only the signature contract is enforced here (not the "cannot override
* private/final" rule), because a trait method is flattened into the class
* and, like a normal subclass method, is allowed to shadow a private parent
* method. PHP reports the incompatibility as a class-declaration fatal error
* ("Declaration of X::m() must be compatible with Y::m()"), which we surface
* at compile time so the broken program is rejected instead of being emitted
* and executed past a runtime fatal error.
*/
private function checkTraitMethodOverrideCompatibility(ClassDef $usingClassDef, MethodDef $methodDef, string $methodName): void
{
if ($methodName === '__construct' || $methodDef->node === null) {
return;
}
$classDef = $usingClassDef;
while (true) {
$extends = $classDef->extends;
if (!$extends) {
break;
}
if ($classDef->inheritedFromInternalClass) {
$modifiers = Reflection::getClassMethodModifiers($extends, $methodName);
if ($modifiers !== null && ($modifiers & \ReflectionMethod::IS_FINAL)) {
$this->fatalError($methodDef->node, "Cannot override final method `{$extends}::{$methodName}()`");
}
break;
}
// Dynamically supplied parents are opaque to the compiler.
if (!$this->hasClass($extends)) {
break;
}
$classDef = $this->getClass($extends);
if ($classDef->hasMethod($methodName)) {
$parentMethodDef = $classDef->getMethod($methodName);
// A private method is a separate slot and may be shadowed by the
// method imported from the trait.
if ($parentMethodDef->flags & Modifiers::PRIVATE) {
break;
}
if ($parentMethodDef->flags & Modifiers::FINAL) {
$this->fatalError($methodDef->node, "Cannot override final method `{$extends}::{$methodName}()`");
}
$this->validateMethodOverrideSignature(
$methodDef->node,
$methodName,
$methodDef,
$parentMethodDef,
$extends
);
break;
}
if ($classDef->hasAbstractMethod($methodName) && isset($classDef->abstractMethodDefs[strtolower($methodName)])) {
$this->validateMethodOverrideSignature(
$methodDef->node,
$methodName,
$methodDef,
$classDef->getAbstractMethod($methodName),
$extends
);
break;
}
}
}
private function isCompatibleTraitConstant(ConstantDef $existing, ConstantDef $incoming): bool
{
return $existing->flags === $incoming->flags &&
$existing->type === $incoming->type &&
$existing->class === $incoming->class &&
$existing->value === $incoming->value;
}
private function isCompatibleTraitProperty(PropertyDef $existing, PropertyDef $incoming): bool
{
return $existing->flags === $incoming->flags &&
$existing->type === $incoming->type &&
$existing->class === $incoming->class &&
$existing->nullable === $incoming->nullable &&
$existing->default === $incoming->default;
}
private function getRegisterClassFunctionArgDef(ClassDef|InterfaceDef $classDef): string private function getRegisterClassFunctionArgDef(ClassDef|InterfaceDef $classDef): string
{ {
$depsCeList = $this->getRegisterClassFunctionCeList($classDef); $depsCeList = $this->getRegisterClassFunctionCeList($classDef);

@ -4637,6 +4637,12 @@ class FileInfo {
continue; continue;
} }
// TypePHP traits are compile-time AST templates and must not become
// Zend class entries in the generated arginfo registration code.
if ($stmt instanceof Trait_) {
continue;
}
if ($stmt instanceof Stmt\ClassLike) { if ($stmt instanceof Stmt\ClassLike) {
$className = $stmt->namespacedName; $className = $stmt->namespacedName;
$constInfos = []; $constInfos = [];
@ -4645,7 +4651,7 @@ class FileInfo {
$enumCaseInfos = []; $enumCaseInfos = [];
if (!$stmt instanceof PhpParser\Node\Stmt\Interface_) { if (!$stmt instanceof PhpParser\Node\Stmt\Interface_) {
getTranslator()->parseTraitUseForStub($stmt, $className); getTranslator()->composeTraitAst($stmt, $className);
} }
ClassInfo::$currentClass = $className->toString(); ClassInfo::$currentClass = $className->toString();

@ -40,7 +40,7 @@ bool(true)
pick:interface pick:interface
bool(true) bool(true)
pick:trait pick:trait
bool(true) bool(false)
pick:enum pick:enum
bool(true) bool(true)
bool(false) bool(false)

@ -20,7 +20,7 @@ function main() {
var_dump(interface_exists("MyInterface")); var_dump(interface_exists("MyInterface"));
var_dump(interface_exists("NonexistentInterface")); var_dump(interface_exists("NonexistentInterface"));
// trait_exists // TypePHP traits are compile-time-only AST templates.
var_dump(trait_exists("MyTrait")); var_dump(trait_exists("MyTrait"));
var_dump(trait_exists("NonexistentTrait")); var_dump(trait_exists("NonexistentTrait"));
@ -40,7 +40,7 @@ bool(false)
bool(false) bool(false)
bool(true) bool(true)
bool(false) bool(false)
bool(true) bool(false)
bool(false) bool(false)
bool(true) bool(true)
bool(false) bool(false)

@ -10,7 +10,7 @@ trait HelloTrait {
} }
function main() { function main() {
eval("(new class { use HelloTrait;})->hello();"); (new class { use HelloTrait; })->hello();
} }
?> ?>
--EXPECT-- --EXPECT--

@ -0,0 +1,52 @@
--TEST--
Trait methods are compiled as class methods for every consuming class
--FILE--
<?php
trait InnerTemplate {
protected int $value = 0;
public function className(): string {
return __CLASS__;
}
public function traitName(): string {
return __TRAIT__;
}
public function setValue(int $value): void {
$this->value = $value;
}
public function getValue(): int {
return $this->value;
}
}
trait OuterTemplate {
use InnerTemplate;
}
class FirstConsumer {
use OuterTemplate;
}
class SecondConsumer {
use OuterTemplate { getValue as readValue; }
}
function main() {
$first = new FirstConsumer();
$second = new SecondConsumer();
$first->setValue(11);
$second->setValue(22);
var_dump($first->className(), $first->traitName(), $first->getValue());
var_dump($second->className(), $second->traitName(), $second->readValue());
}
?>
--EXPECT--
string(13) "FirstConsumer"
string(13) "InnerTemplate"
int(11)
string(14) "SecondConsumer"
string(13) "InnerTemplate"
int(22)

@ -0,0 +1,55 @@
--TEST--
Trait methods retain their declaring file import context after AST composition
--FILE--
<?php
namespace TraitImports\Support {
class Marker {
public string $value = 'class';
}
function imported_label(): string {
return 'function';
}
const IMPORTED_VALUE = 'constant';
}
namespace TraitImports\Template {
use TraitImports\Support\Marker as ImportedMarker;
use function TraitImports\Support\imported_label as importedLabel;
use const TraitImports\Support\IMPORTED_VALUE as importedValue;
trait ImportedNames {
public function values(): array {
$marker = new ImportedMarker();
return [$marker->value, importedLabel(), importedValue, self::SELF_VALUE];
}
}
}
namespace TraitImports\Consumer {
use TraitImports\Template\ImportedNames;
class Example {
use ImportedNames;
public const string SELF_VALUE = 'self';
}
}
namespace {
function main(): void {
var_dump((new TraitImports\Consumer\Example())->values());
}
}
?>
--EXPECT--
array(4) {
[0]=>
string(5) "class"
[1]=>
string(8) "function"
[2]=>
string(8) "constant"
[3]=>
string(4) "self"
}

@ -18,7 +18,7 @@ function main() {
echo interface_exists("NonExistentInterface") ? "fail\n" : "ok-false\n"; echo interface_exists("NonExistentInterface") ? "fail\n" : "ok-false\n";
echo "trait_exists:\n"; echo "trait_exists:\n";
echo trait_exists("MyTrait") ? "ok-true\n" : "fail\n"; echo trait_exists("MyTrait") ? "fail\n" : "ok-false\n";
echo trait_exists("NonExistentTrait") ? "fail\n" : "ok-false\n"; echo trait_exists("NonExistentTrait") ? "fail\n" : "ok-false\n";
echo "enum_exists:\n"; echo "enum_exists:\n";
@ -36,7 +36,7 @@ interface_exists:
ok-true ok-true
ok-false ok-false
trait_exists: trait_exists:
ok-true ok-false
ok-false ok-false
enum_exists: enum_exists:
ok-true ok-true

Loading…
Cancel
Save