From 5fffaaad02ba32b93e75886f158d0b5cbb96af3e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Nov 2026 13:43:00 +0000 Subject: [PATCH 1/3] =?UTF-8?q?fix(reflection):=20=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E6=B3=A8=E8=A7=A3=20AST=20=E4=BB=A5=E6=94=AF=E6=8C=81=20getArg?= =?UTF-8?q?uments/newInstance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在类/方法/属性/参数上与应用实例并行存储 Annotation 声明节点, ReflectionAttribute 据此还原用户实参并在 newInstance 后绑定可读属性; 放宽对无 PHP #[Attribute] 元数据的 std 原生注解校验。 Co-authored-by: selden --- cmd/compile/special_handlers.go | 4 +- node/class.go | 27 ++- node/class_abstract.go | 4 +- node/class_declaration.go | 3 +- node/class_register.go | 6 +- node/function.go | 5 +- parser/annotation_apply.go | 2 +- parser/annotation_parser.go | 2 +- parser/class_parser.go | 8 +- std/channel/channel_class.go | 3 +- std/loop/hashmap_class.go | 3 +- std/loop/list_class.go | 3 +- std/php/reflection/annotation_bind.go | 114 +++++++++++++ std/php/reflection/get_attributes.go | 17 +- .../reflection_attribute_factory.go | 55 ++++++ .../reflection_attribute_get_arguments.go | 21 +-- .../reflection_attribute_new_instance.go | 26 ++- .../reflection/reflection_attribute_test.go | 48 ++++++ .../reflection_method_get_attributes.go | 6 +- .../reflection_parameter_get_attributes.go | 6 +- .../reflection_property_get_attributes.go | 6 +- std/signal/signal_channel_class.go | 2 +- tests/php/reflection_attribute_args_test.php | 156 ++++++++++++++++++ 23 files changed, 455 insertions(+), 72 deletions(-) create mode 100644 std/php/reflection/annotation_bind.go create mode 100644 std/php/reflection/reflection_attribute_factory.go create mode 100644 std/php/reflection/reflection_attribute_test.go create mode 100644 tests/php/reflection_attribute_args_test.php diff --git a/cmd/compile/special_handlers.go b/cmd/compile/special_handlers.go index 3738dd84..075cf0b8 100644 --- a/cmd/compile/special_handlers.go +++ b/cmd/compile/special_handlers.go @@ -326,7 +326,7 @@ func (g *Generator) emitClassStatementInit(n *node.ClassStatement) error { if err := g.emitClassAnnotation(ann); err != nil { return err } - g.printf(")\n") + g.printf(", nil)\n") } return nil } @@ -1027,7 +1027,7 @@ func (g *Generator) emitClassMethod(method data.Method) error { if err := g.emitClassAnnotation(ann); err != nil { return err } - g.printf(")\n") + g.printf(", nil)\n") } g.printf("return m\n") g.indent-- diff --git a/node/class.go b/node/class.go index d58d5835..84d93ca9 100644 --- a/node/class.go +++ b/node/class.go @@ -32,6 +32,7 @@ type ClassStatement struct { initialization atomic.Pointer[classInitialization] privateLookup atomic.Pointer[privateMethodIndex] Annotations []*data.ClassValue // 类注解列表 + AnnotationDecls []*Annotation // 与 Annotations 下标对应的源码声明 // 构造函数 Construct data.Method @@ -289,11 +290,13 @@ func (c *ClassStatement) GetImplements() []string { return c.Implements } -func (c *ClassStatement) AddAnnotations(a *data.ClassValue) { +func (c *ClassStatement) AddAnnotations(a *data.ClassValue, decl *Annotation) { if c.Annotations == nil { c.Annotations = []*data.ClassValue{} + c.AnnotationDecls = []*Annotation{} } c.Annotations = append(c.Annotations, a) + c.AnnotationDecls = append(c.AnnotationDecls, decl) } func (c *ClassStatement) GetPropertyList() []data.Property { @@ -572,7 +575,8 @@ type ClassProperty struct { Virtual bool IsPromoted bool // 是否是构造函数参数属性提升 DefaultValue data.GetValue // 默认值 - Annotations []*data.ClassValue // 属性注解列表 + Annotations []*data.ClassValue // 属性注解列表 + AnnotationDecls []*Annotation // 与 Annotations 下标对应的源码声明 Type data.TypeRef // 属性类型 IsConstant bool DeclaringClass string @@ -744,11 +748,13 @@ func (p *ClassProperty) GetIsStatic() bool { return p.IsStatic } -func (p *ClassProperty) AddAnnotations(a *data.ClassValue) { +func (p *ClassProperty) AddAnnotations(a *data.ClassValue, decl *Annotation) { if p.Annotations == nil { p.Annotations = []*data.ClassValue{} + p.AnnotationDecls = []*Annotation{} } p.Annotations = append(p.Annotations, a) + p.AnnotationDecls = append(p.AnnotationDecls, decl) } type ClassMethod struct { @@ -760,6 +766,7 @@ type ClassMethod struct { Body []data.GetValue // 方法体 vars []data.Variable Annotations []*data.ClassValue // 方法注解列表 + AnnotationDecls []*Annotation // 与 Annotations 下标对应的源码声明 Ret data.TypeRef // 返回类型 IsGenerator bool // 是否是生成器方法(含 yield) StrictTypes bool @@ -791,11 +798,13 @@ func NewMethod(from data.From, name string, modifier string, isStatic bool, para } } -func (m *ClassMethod) AddAnnotations(a *data.ClassValue) { +func (m *ClassMethod) AddAnnotations(a *data.ClassValue, decl *Annotation) { if m.Annotations == nil { m.Annotations = []*data.ClassValue{} + m.AnnotationDecls = []*Annotation{} } m.Annotations = append(m.Annotations, a) + m.AnnotationDecls = append(m.AnnotationDecls, decl) } func (m *ClassMethod) GetIsStatic() bool { @@ -937,5 +946,13 @@ func checkClassIs(ctx data.Context, source data.ClassStmt, target string) (bool, } type AddAnnotations interface { - AddAnnotations(a *data.ClassValue) + AddAnnotations(a *data.ClassValue, decl *Annotation) +} + +// AnnotationDeclAt 返回与已应用注解实例同下标的源码声明(可能为 nil)。 +func AnnotationDeclAt(decls []*Annotation, index int) *Annotation { + if index < 0 || index >= len(decls) { + return nil + } + return decls[index] } diff --git a/node/class_abstract.go b/node/class_abstract.go index 66ba8e74..3daa655a 100644 --- a/node/class_abstract.go +++ b/node/class_abstract.go @@ -40,6 +40,6 @@ func (c *AbstractClassStatement) GetValue(ctx data.Context) (data.GetValue, data } // AddAnnotations 添加注解(实现 AddAnnotations 接口) -func (c *AbstractClassStatement) AddAnnotations(a *data.ClassValue) { - c.ClassStatement.AddAnnotations(a) +func (c *AbstractClassStatement) AddAnnotations(a *data.ClassValue, decl *Annotation) { + c.ClassStatement.AddAnnotations(a, decl) } diff --git a/node/class_declaration.go b/node/class_declaration.go index dcd1dff2..786ebc40 100644 --- a/node/class_declaration.go +++ b/node/class_declaration.go @@ -20,7 +20,8 @@ func CloneClassDeclaration(source data.ClassStmt) data.ClassStmt { Methods: maps.Clone(core.Methods), StaticMethods: maps.Clone(core.StaticMethods), Construct: core.Construct, IsAbstract: core.IsAbstract, Flags: core.Flags, EnumCases: slices.Clone(core.EnumCases), - Annotations: slices.Clone(core.Annotations), + Annotations: slices.Clone(core.Annotations), + AnnotationDecls: slices.Clone(core.AnnotationDecls), Traits: slices.Clone(core.Traits), DeferredTraits: slices.Clone(core.DeferredTraits), DeferredTraitAliases: slices.Clone(core.DeferredTraitAliases), DeferredTraitsMerged: core.DeferredTraitsMerged, diff --git a/node/class_register.go b/node/class_register.go index b0a20e70..d5874daf 100644 --- a/node/class_register.go +++ b/node/class_register.go @@ -115,9 +115,9 @@ func classStmtFromAny(v data.ClassStmt) *ClassStatement { } // AddAnnotations 转发到内部 ClassStatement(AnnotationParser 以 ClassRegisterStmt 为 next) -func (s *ClassRegisterStmt) AddAnnotations(a *data.ClassValue) { +func (s *ClassRegisterStmt) AddAnnotations(a *data.ClassValue, decl *Annotation) { if s.Class != nil { - s.Class.AddAnnotations(a) + s.Class.AddAnnotations(a, decl) } } @@ -185,7 +185,7 @@ func applyClassAnnotations(vm data.VM, annotations []*Annotation, target AddAnno } } if o, ok := obj.(*data.ClassValue); ok { - target.AddAnnotations(o) + target.AddAnnotations(o, an) } } for i := len(callAnn) - 1; i >= 0; i-- { diff --git a/node/function.go b/node/function.go index 792feda9..78e13b53 100644 --- a/node/function.go +++ b/node/function.go @@ -243,16 +243,19 @@ type Parameter struct { Type data.TypeRef DefaultValue data.GetValue // 默认值 Annotations []*data.ClassValue + AnnotationDecls []*Annotation ConstructorClass string // Set only for parameters declared in a constructor. } func (p *Parameter) SetConstructorClass(name string) { p.ConstructorClass = name } -func (p *Parameter) AddAnnotations(a *data.ClassValue) { +func (p *Parameter) AddAnnotations(a *data.ClassValue, decl *Annotation) { if p.Annotations == nil { p.Annotations = []*data.ClassValue{} + p.AnnotationDecls = []*Annotation{} } p.Annotations = append(p.Annotations, a) + p.AnnotationDecls = append(p.AnnotationDecls, decl) } func (p *Parameter) GetDefaultValue() data.GetValue { diff --git a/parser/annotation_apply.go b/parser/annotation_apply.go index 81479fbc..961c1cbf 100644 --- a/parser/annotation_apply.go +++ b/parser/annotation_apply.go @@ -42,7 +42,7 @@ func ApplyAnnotations(p *Parser, target node.AddAnnotations, annotations []*node } } if o, ok := obj.(*data.ClassValue); ok { - target.AddAnnotations(o) + target.AddAnnotations(o, an) } } for i := len(callAnn) - 1; i >= 0; i-- { diff --git a/parser/annotation_parser.go b/parser/annotation_parser.go index 87c44804..1ef98e1c 100644 --- a/parser/annotation_parser.go +++ b/parser/annotation_parser.go @@ -201,7 +201,7 @@ func (p *AnnotationParser) Parse() (data.GetValue, data.Control) { } if c, ok := next.(node.AddAnnotations); ok { if o, ok := obj.(*data.ClassValue); ok { - c.AddAnnotations(o) + c.AddAnnotations(o, an) } } } diff --git a/parser/class_parser.go b/parser/class_parser.go index e116a376..0334ed30 100644 --- a/parser/class_parser.go +++ b/parser/class_parser.go @@ -334,7 +334,7 @@ func callClassAnnotation(p *Parser, ans *[]*node.Annotation, c node.AddAnnotatio return acl } if o, ok := obj.(*data.ClassValue); ok { - c.AddAnnotations(o) + c.AddAnnotations(o, an) } } return nil @@ -612,7 +612,7 @@ func (p *ClassParser) parsePropertyWithAnnotations(modifier string, isStatic boo } } if o, ok := obj.(*data.ClassValue); ok { - ret.AddAnnotations(o) + ret.AddAnnotations(o, an) } } for i := len(callAnn) - 1; i >= 0; i-- { @@ -702,7 +702,7 @@ func (p *ClassParser) parsePropertyWithAnnotations(modifier string, isStatic boo } } if o, ok := obj.(*data.ClassValue); ok { - ret.AddAnnotations(o) + ret.AddAnnotations(o, an) } } for i := len(callAnn) - 1; i >= 0; i-- { @@ -878,7 +878,7 @@ func (p *ClassParser) parseMethodWithAnnotations(modifier string, isStatic bool, } if c, ok := ret.(node.AddAnnotations); ok { if o, ok := obj.(*data.ClassValue); ok { - c.AddAnnotations(o) + c.AddAnnotations(o, an) } } } diff --git a/std/channel/channel_class.go b/std/channel/channel_class.go index f25d8c22..a8c63f2e 100644 --- a/std/channel/channel_class.go +++ b/std/channel/channel_class.go @@ -2,6 +2,7 @@ package channel import ( "github.com/php-any/origami/data" + "github.com/php-any/origami/node" ) // ChannelClass 表示 Channel 类 @@ -93,6 +94,6 @@ func (c *ChannelClass) GetConstruct() data.Method { } // AddAnnotations 添加注解 -func (c *ChannelClass) AddAnnotations(a *data.ClassValue) { +func (c *ChannelClass) AddAnnotations(a *data.ClassValue, _ *node.Annotation) { // 暂时不处理注解 } diff --git a/std/loop/hashmap_class.go b/std/loop/hashmap_class.go index 730abb51..f9ec3e2a 100644 --- a/std/loop/hashmap_class.go +++ b/std/loop/hashmap_class.go @@ -2,6 +2,7 @@ package loop import ( "github.com/php-any/origami/data" + "github.com/php-any/origami/node" ) // HashMap 泛型哈希表结构 @@ -325,6 +326,6 @@ func (h *HashMapClass) GetConstruct() data.Method { } // AddAnnotations 添加注解 -func (h *HashMapClass) AddAnnotations(a *data.ClassValue) { +func (h *HashMapClass) AddAnnotations(a *data.ClassValue, _ *node.Annotation) { // 暂时不处理注解 } diff --git a/std/loop/list_class.go b/std/loop/list_class.go index 46b13782..0807e6cc 100644 --- a/std/loop/list_class.go +++ b/std/loop/list_class.go @@ -2,6 +2,7 @@ package loop import ( "github.com/php-any/origami/data" + "github.com/php-any/origami/node" ) // List 泛型列表结构 @@ -318,6 +319,6 @@ func (l *ListClass) GetConstruct() data.Method { } // AddAnnotations 添加注解 -func (l *ListClass) AddAnnotations(a *data.ClassValue) { +func (l *ListClass) AddAnnotations(a *data.ClassValue, _ *node.Annotation) { // 暂时不处理注解 } diff --git a/std/php/reflection/annotation_bind.go b/std/php/reflection/annotation_bind.go new file mode 100644 index 00000000..df4c0b34 --- /dev/null +++ b/std/php/reflection/annotation_bind.go @@ -0,0 +1,114 @@ +package reflection + +import ( + "github.com/php-any/origami/data" + "github.com/php-any/origami/node" +) + +func isAnnotationTargetParam(param data.GetValue) bool { + switch p := param.(type) { + case *node.AnnotationTargetParameter: + return true + case *node.Parameter: + return p.Name == node.TargetName + default: + return false +} +} + +// bindUserAnnotationProperties 将注解声明中的用户参数写入实例属性, +// 便于 newInstance() 后通过 $instance->prop 读取(兼容无 GetPropertyList 的原生注解类)。 +func bindUserAnnotationProperties(ctx data.Context, inst *data.ClassValue, decl *node.Annotation) { + if inst == nil || decl == nil || inst.Class == nil { + return + } + construct := inst.Class.GetConstruct() + if construct == nil { + return + } + params := construct.GetParams() + if len(params) == 0 { + return + } + + pos := 0 + for _, arg := range decl.Arguments { + switch a := arg.(type) { + case *node.NamedArgument: + if a.Name == node.TargetName { + continue + } + val, ctl := a.GetValue(ctx) + if ctl != nil || val == nil { + continue + } + if v, ok := val.(data.Value); ok { + inst.SetProperty(a.Name, v) + } + default: + val, ctl := arg.GetValue(ctx) + if ctl != nil || val == nil { + continue + } + v, ok := val.(data.Value) + if !ok { + continue + } + for pos < len(params) { + if isAnnotationTargetParam(params[pos]) { + pos++ + continue + } + name := constructorParamPropertyName(params[pos]) + if name != "" { + inst.SetProperty(name, v) + } + pos++ + break + } + } + } +} + +func constructorParamPropertyName(param data.GetValue) string { + switch p := param.(type) { + case *node.PromotedParameter: + return p.PropertyName + case *node.Parameter: + return p.Name + default: + if v, ok := param.(data.Variable); ok { + return v.GetName() + } + } + return "" +} + +// userAnnotationArguments 把声明中的实参求值为 getArguments() 应返回的数组(排除编译器注入的 target)。 +func userAnnotationArguments(ctx data.Context, decl *node.Annotation) (data.Value, data.Control) { + array := data.NewArrayValueFromSlots(nil) + if decl == nil { + return array, nil + } + for _, argument := range decl.Arguments { + if named, ok := argument.(*node.NamedArgument); ok { + if named.Name == node.TargetName { + continue + } + } + raw, ctl := argument.GetValue(ctx) + if ctl != nil { + return nil, ctl + } + value, ok := raw.(data.Value) + if !ok || value == nil { + value = data.NewNullValue() + } + if named, ok := argument.(*node.NamedArgument); ok { + array.SetStringKey(named.Name, value) + } else { + array.SetKey(data.NewIntValue(array.NextAppendIntKey()), value) + } + } + return array, nil +} diff --git a/std/php/reflection/get_attributes.go b/std/php/reflection/get_attributes.go index eaf76d5f..52786d05 100644 --- a/std/php/reflection/get_attributes.go +++ b/std/php/reflection/get_attributes.go @@ -37,17 +37,6 @@ func (m *ReflectionClassGetAttributesMethod) GetReturnType() data.Types { return data.Arrays{} } -// newReflectionAttribute 创建一个新的 ReflectionAttribute 实例 -func newReflectionAttribute(ctx data.Context, annotation *data.ClassValue) *data.ClassValue { - attrClass := &ReflectionAttributeClass{} - attrValue := data.NewClassValue(attrClass, ctx.CreateBaseContext()) - - // 存储注解对象到实例属性中 - attrValue.ObjectValue.SetProperty("_annotation", annotation) - - return attrValue -} - func (m *ReflectionClassGetAttributesMethod) Call(ctx data.Context) (data.GetValue, data.Control) { // 获取被反射的类信息 _, classStmt := getReflectionClassInfo(ctx) @@ -85,7 +74,7 @@ func (m *ReflectionClassGetAttributesMethod) Call(ctx data.Context) (data.GetVal } } - for _, annotation := range classStatement.Annotations { + for i, annotation := range classStatement.Annotations { if filterName != "" { annName := annotation.Class.GetName() if instanceof { @@ -96,7 +85,9 @@ func (m *ReflectionClassGetAttributesMethod) Call(ctx data.Context) (data.GetVal continue } } - attrValue := newReflectionAttribute(ctx, annotation) + decl := node.AnnotationDeclAt(classStatement.AnnotationDecls, i) + repeated := attributeIsRepeatedAt(classStatement.Annotations, i) + attrValue := newReflectionAttribute(ctx, annotation, decl, reflectionAttributeTargetClass, repeated) attributes = append(attributes, attrValue) } } diff --git a/std/php/reflection/reflection_attribute_factory.go b/std/php/reflection/reflection_attribute_factory.go new file mode 100644 index 00000000..e97d7eb1 --- /dev/null +++ b/std/php/reflection/reflection_attribute_factory.go @@ -0,0 +1,55 @@ +package reflection + +import ( + "strings" + + "github.com/php-any/origami/data" + "github.com/php-any/origami/node" +) + +const ( + reflectionAttributeTargetClass = 1 + reflectionAttributeTargetFunction = 2 + reflectionAttributeTargetMethod = 4 + reflectionAttributeTargetProperty = 8 + reflectionAttributeTargetParameter = 16 +) + +// newReflectionAttribute 创建 ReflectionAttribute 实例;decl 为源码注解节点(可为 nil)。 +func newReflectionAttribute(ctx data.Context, annotation *data.ClassValue, decl *node.Annotation, target int, repeated bool) *data.ClassValue { + if decl != nil && annotation != nil { + // 保持与运行时注解实例一致,供无声明时的回退路径使用 + _ = annotation + } + attrClass := &ReflectionAttributeClass{ + declaration: decl, + target: target, + repeated: repeated, + } + attrValue := data.NewClassValue(attrClass, ctx.CreateBaseContext()) + if annotation != nil { + attrValue.ObjectValue.SetProperty("_annotation", annotation) + } + return attrValue +} + +func attributeIsRepeatedAt(annotations []*data.ClassValue, index int) bool { + if index < 0 || index >= len(annotations) || annotations[index] == nil || annotations[index].Class == nil { + return false + } + name := strings.ToLower(annotations[index].Class.GetName()) + seen := 0 + for i, ann := range annotations { + if ann == nil || ann.Class == nil { + continue + } + if strings.ToLower(ann.Class.GetName()) != name { + continue + } + if i == index { + return seen > 0 + } + seen++ + } + return false +} diff --git a/std/php/reflection/reflection_attribute_get_arguments.go b/std/php/reflection/reflection_attribute_get_arguments.go index ac8bc30b..959bfe19 100644 --- a/std/php/reflection/reflection_attribute_get_arguments.go +++ b/std/php/reflection/reflection_attribute_get_arguments.go @@ -2,7 +2,6 @@ package reflection import ( "github.com/php-any/origami/data" - "github.com/php-any/origami/node" ) // ReflectionAttributeGetArgumentsMethod 实现 ReflectionAttribute::getArguments @@ -43,30 +42,12 @@ func (m *ReflectionAttributeGetArgumentsMethod) GetReturnType() data.Types { // 返回传递给属性(注解)的参数数组 func (m *ReflectionAttributeGetArgumentsMethod) Call(ctx data.Context) (data.GetValue, data.Control) { if annotation := reflectionAttributeDeclaration(ctx); annotation != nil { - array := data.NewArrayValueFromSlots(nil) - for _, argument := range annotation.Arguments { - raw, ctl := argument.GetValue(ctx) - if ctl != nil { - return nil, ctl - } - value := raw.(data.Value) - if named, ok := argument.(*node.NamedArgument); ok { - array.SetStringKey(named.Name, value) - } else { - array.SetKey(data.NewIntValue(array.NextAppendIntKey()), value) - } - } - return array, nil + return userAnnotationArguments(ctx, annotation) } annotationValue := getReflectionAttributeInfo(ctx) if annotationValue == nil { return data.NewArrayValue([]data.Value{}), nil } - // 注解对象是 ClassValue,但我们已经实例化了注解 - // 原始参数已经被传递给构造函数并使用了 - // 由于 ClassStatement.Annotations 只存储了 ClassValue,我们无法直接获取原始参数 - // 这里返回空数组,因为参数已经在实例化时被使用了 - // TODO: 如果需要支持 getArguments,需要在 ClassStatement 中同时存储 Annotation 节点 return data.NewArrayValue([]data.Value{}), nil } diff --git a/std/php/reflection/reflection_attribute_new_instance.go b/std/php/reflection/reflection_attribute_new_instance.go index 7ebb3c27..2ffb7f14 100644 --- a/std/php/reflection/reflection_attribute_new_instance.go +++ b/std/php/reflection/reflection_attribute_new_instance.go @@ -63,18 +63,26 @@ func (m *ReflectionAttributeNewInstanceMethod) Call(ctx data.Context) (data.GetV } } } - if flags < 0 { - return nil, data.NewErrorThrowByName(nil, errors.New("Attempting to use non-attribute class "+annotation.Name+" as attribute"), "Error") - } - if flags&metadata.target == 0 { - return nil, data.NewErrorThrowByName(nil, errors.New("Attribute "+annotation.Name+" cannot target function"), "Error") - } - if metadata.repeated && flags&64 == 0 { - return nil, data.NewErrorThrowByName(nil, errors.New("Attribute "+annotation.Name+" must not be repeated"), "Error") + // 仅有 PHP 类上的 #[Attribute] 元数据时才做目标/可重复性校验; + // std 原生注解类无该元数据,但已在运行时成功挂载,允许 newInstance。 + if flags >= 0 { + if flags&metadata.target == 0 { + return nil, data.NewErrorThrowByName(nil, errors.New("Attribute "+annotation.Name+" cannot target function"), "Error") + } + if metadata.repeated && flags&64 == 0 { + return nil, data.NewErrorThrowByName(nil, errors.New("Attribute "+annotation.Name+" must not be repeated"), "Error") + } } } } - return node.CreateInstanceFromClass(class, annotation.Arguments, ctx) + instance, ctl := node.CreateInstanceFromClass(class, annotation.Arguments, ctx) + if ctl != nil { + return nil, ctl + } + if cv, ok := instance.(*data.ClassValue); ok { + bindUserAnnotationProperties(ctx, cv, annotation) + } + return instance, nil } annotationValue := getReflectionAttributeInfo(ctx) if annotationValue == nil { diff --git a/std/php/reflection/reflection_attribute_test.go b/std/php/reflection/reflection_attribute_test.go new file mode 100644 index 00000000..7b30ae0a --- /dev/null +++ b/std/php/reflection/reflection_attribute_test.go @@ -0,0 +1,48 @@ +package reflection + +import ( + "testing" + + "github.com/php-any/origami/data" + "github.com/php-any/origami/node" + "github.com/php-any/origami/parser" + "github.com/php-any/origami/runtime" +) + +func TestUserAnnotationArgumentsMixedPositionalNamed(t *testing.T) { + vm := runtime.NewVM(parser.NewParser()) + ctx := vm.CreateContext(nil) + decl := node.NewAnnotation(nil, "Mark", []data.GetValue{ + node.NewNamedArgument(nil, "count", node.NewIntLiteral(nil, "7")), + }) + decl.Arguments = append([]data.GetValue{node.NewStringLiteral(nil, "mix")}, decl.Arguments...) + + arr, ctl := userAnnotationArguments(ctx, decl) + if ctl != nil { + t.Fatalf("userAnnotationArguments: %v", ctl) + } + av := arr.(*data.ArrayValue) + if slot, ok := av.LookupZValByStringKey("count"); !ok || slot.ReadValue().AsString() != "7" { + t.Fatalf("count=%#v", slot) + } + if av.Len() < 1 || av.At(0).ReadValue().AsString() != "mix" { + t.Fatalf("positional=%v", av.ToValueList()) + } +} + +func TestClassStatementStoresAnnotationDeclaration(t *testing.T) { + vm := runtime.NewVM(parser.NewParser()) + decl := node.NewAnnotation(nil, "Table", []data.GetValue{ + node.NewNamedArgument(nil, "name", node.NewStringLiteral(nil, "users")), + }) + inst := data.NewClassValue(&node.ClassStatement{Name: "Table"}, vm.CreateContext(nil)) + cls := &node.ClassStatement{Name: "App\\Entity"} + cls.AddAnnotations(inst, decl) + if len(cls.AnnotationDecls) != 1 || cls.AnnotationDecls[0] != decl { + t.Fatalf("AnnotationDecls not stored: %#v", cls.AnnotationDecls) + } + got := node.AnnotationDeclAt(cls.AnnotationDecls, 0) + if got != decl { + t.Fatalf("AnnotationDeclAt mismatch") + } +} diff --git a/std/php/reflection/reflection_method_get_attributes.go b/std/php/reflection/reflection_method_get_attributes.go index 3737188f..5efb428b 100644 --- a/std/php/reflection/reflection_method_get_attributes.go +++ b/std/php/reflection/reflection_method_get_attributes.go @@ -58,7 +58,7 @@ func (m *ReflectionMethodGetAttributesMethod) Call(ctx data.Context) (data.GetVa instanceof := flags&2 != 0 attributes := make([]data.Value, 0, len(method.Annotations)) - for _, annotation := range method.Annotations { + for i, annotation := range method.Annotations { if annotation == nil { continue } @@ -71,7 +71,9 @@ func (m *ReflectionMethodGetAttributesMethod) Call(ctx data.Context) (data.GetVa continue } } - attributes = append(attributes, newReflectionAttribute(ctx, annotation)) + decl := node.AnnotationDeclAt(method.AnnotationDecls, i) + repeated := attributeIsRepeatedAt(method.Annotations, i) + attributes = append(attributes, newReflectionAttribute(ctx, annotation, decl, reflectionAttributeTargetMethod, repeated)) } return data.NewArrayValue(attributes), nil } diff --git a/std/php/reflection/reflection_parameter_get_attributes.go b/std/php/reflection/reflection_parameter_get_attributes.go index 95e552f6..ff80a5e4 100644 --- a/std/php/reflection/reflection_parameter_get_attributes.go +++ b/std/php/reflection/reflection_parameter_get_attributes.go @@ -53,14 +53,16 @@ func (m *ReflectionParameterGetAttributesMethod) Call(ctx data.Context) (data.Ge } attributes := make([]data.Value, 0, len(param.Annotations)) - for _, annotation := range param.Annotations { + for i, annotation := range param.Annotations { if annotation == nil || annotation.Class == nil { continue } if filterName != "" && annotation.Class.GetName() != filterName { continue } - attributes = append(attributes, newReflectionAttribute(ctx, annotation)) + decl := node.AnnotationDeclAt(param.AnnotationDecls, i) + repeated := attributeIsRepeatedAt(param.Annotations, i) + attributes = append(attributes, newReflectionAttribute(ctx, annotation, decl, reflectionAttributeTargetParameter, repeated)) } return data.NewArrayValue(attributes), nil } diff --git a/std/php/reflection/reflection_property_get_attributes.go b/std/php/reflection/reflection_property_get_attributes.go index dca0535a..2ad5149b 100644 --- a/std/php/reflection/reflection_property_get_attributes.go +++ b/std/php/reflection/reflection_property_get_attributes.go @@ -58,7 +58,7 @@ func (m *ReflectionPropertyGetAttributesMethod) Call(ctx data.Context) (data.Get instanceof := flags&2 != 0 attributes := make([]data.Value, 0, len(property.Annotations)) - for _, annotation := range property.Annotations { + for i, annotation := range property.Annotations { if annotation == nil { continue } @@ -71,7 +71,9 @@ func (m *ReflectionPropertyGetAttributesMethod) Call(ctx data.Context) (data.Get continue } } - attributes = append(attributes, newReflectionAttribute(ctx, annotation)) + decl := node.AnnotationDeclAt(property.AnnotationDecls, i) + repeated := attributeIsRepeatedAt(property.Annotations, i) + attributes = append(attributes, newReflectionAttribute(ctx, annotation, decl, reflectionAttributeTargetProperty, repeated)) } return data.NewArrayValue(attributes), nil } diff --git a/std/signal/signal_channel_class.go b/std/signal/signal_channel_class.go index 2db0707e..44c99056 100644 --- a/std/signal/signal_channel_class.go +++ b/std/signal/signal_channel_class.go @@ -62,7 +62,7 @@ func (c *SignalChannelClass) GetConstruct() data.Method { return &SignalChannelConstructMethod{source: c} } -func (c *SignalChannelClass) AddAnnotations(a *data.ClassValue) {} +func (c *SignalChannelClass) AddAnnotations(a *data.ClassValue, _ *node.Annotation) {} type SignalChannelConstructMethod struct { source *SignalChannelClass diff --git a/tests/php/reflection_attribute_args_test.php b/tests/php/reflection_attribute_args_test.php new file mode 100644 index 00000000..6c9be5fe --- /dev/null +++ b/tests/php/reflection_attribute_args_test.php @@ -0,0 +1,156 @@ +getArguments(); + if (!is_array($args) || !array_key_exists($key, $args)) { + \Log::fatal("getArguments 缺少键 {$key}: " . var_export($args, true)); + } + if ($args[$key] !== $expected) { + \Log::fatal("getArguments[{$key}] 期望 " . var_export($expected, true) . ',实际 ' . var_export($args[$key], true)); + } +} + +// --- Table --- +$entityRef = new \ReflectionClass(ReflectionAttr_Entity::class); +$tableAttrs = $entityRef->getAttributes(Table::class); +if (count($tableAttrs) !== 1) { + \Log::fatal('Table 注解数量应为 1'); +} +assertArgsNonEmpty($tableAttrs[0], 'name', 'reflection_attr_users'); +$tableInst = $tableAttrs[0]->newInstance(); +if ($tableInst->name !== 'reflection_attr_users') { + \Log::fatal('Table newInstance()->name 错误: ' . var_export($tableInst->name, true)); +} + +// --- Column 回归 --- +$colProp = $entityRef->getProperty('id'); +$colAttrs = $colProp->getAttributes(Column::class); +if (count($colAttrs) !== 1) { + \Log::fatal('Column 注解数量应为 1'); +} +assertArgsNonEmpty($colAttrs[0], 'name', 'id'); +$colInst = $colAttrs[0]->newInstance(); +if ($colInst->name !== 'id') { + \Log::fatal('Column newInstance()->name 错误'); +} + +// --- Middleware + Singleton --- +$svcRef = new \ReflectionClass(ReflectionAttr_Service::class); +$mwAttrs = $svcRef->getAttributes(Middleware::class); +if (count($mwAttrs) !== 1) { + \Log::fatal('Middleware 注解缺失'); +} +assertArgsNonEmpty($mwAttrs[0], 'className', 'App\\AuthMiddleware'); +$mwInst = $mwAttrs[0]->newInstance(); +if ($mwInst->className !== 'App\\AuthMiddleware') { + \Log::fatal('Middleware newInstance()->className 为空或错误'); +} +// --- GetMapping / Command --- +$ctrlRef = new \ReflectionClass(ReflectionAttr_Controller::class); +$hello = $ctrlRef->getMethod('hello'); +$mapAttrs = $hello->getAttributes(GetMapping::class); +if (count($mapAttrs) !== 1) { + \Log::fatal('GetMapping 注解缺失'); +} +assertArgsNonEmpty($mapAttrs[0], 'path', '/reflection/hello'); +$mapInst = $mapAttrs[0]->newInstance(); +if ($mapInst->path !== '/reflection/hello') { + \Log::fatal('GetMapping newInstance()->path 错误: ' . var_export($mapInst->path, true)); +} + +$ping = $ctrlRef->getMethod('ping'); +$cmdAttrs = $ping->getAttributes(Command::class); +if (count($cmdAttrs) !== 1) { + \Log::fatal('Command 注解缺失'); +} +assertArgsNonEmpty($cmdAttrs[0], 'name', 'reflect:ping'); +assertArgsNonEmpty($cmdAttrs[0], 'description', 'ping command'); +$cmdInst = $cmdAttrs[0]->newInstance(); +if ($cmdInst->name !== 'reflect:ping' || $cmdInst->description !== 'ping command') { + \Log::fatal('Command newInstance 属性错误'); +} + +// --- 用户自定义属性:混用位置参数与命名参数 --- +$mixedRef = new \ReflectionClass(ReflectionAttr_MixedArgs::class); +$customAttrs = $mixedRef->getAttributes(ReflectionAttr_CustomMark::class); +if (count($customAttrs) !== 2) { + \Log::fatal('自定义重复属性数量应为 2'); +} +$argsNamed = $customAttrs[0]->getArguments(); +$labelArg = $argsNamed['label'] ?? ($argsNamed[0] ?? null); +if ($labelArg !== 'mix' || !isset($argsNamed['count']) || $argsNamed['count'] !== 7) { + \Log::fatal('混合命名参数 getArguments 错误: ' . var_export($argsNamed, true)); +} +$instNamed = $customAttrs[0]->newInstance(); +if ($instNamed->label !== 'mix' || $instNamed->count !== 7) { + \Log::fatal('用户属性 newInstance 命名参数失败'); +} +$argsPos = $customAttrs[1]->getArguments(); +if (!isset($argsPos[0]) || $argsPos[0] !== 'first') { + \Log::fatal('位置参数 getArguments 错误: ' . var_export($argsPos, true)); +} + +// getArguments 不得包含编译器注入的 target(std 注解构造函数的第二形参) +foreach ([$tableAttrs[0], $mapAttrs[0], $mwAttrs[0]] as $stdAttr) { + $a = $stdAttr->getArguments(); + if (array_key_exists('target', $a)) { + \Log::fatal('getArguments 不得包含 target: ' . var_export($a, true)); + } +} + +\Log::info('reflection_attribute_args 测试通过'); From 31d6ae9b0fc67b3243b972fe488d5edb8d178083 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Nov 2026 13:48:41 +0000 Subject: [PATCH 2/3] =?UTF-8?q?fix(reflection):=20newInstance=20=E4=BB=85?= =?UTF-8?q?=E5=AF=B9=20PHP=20ClassStatement=20=E6=A0=A1=E9=AA=8C=20Attribu?= =?UTF-8?q?te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 恢复无 #[Attribute] 的 PHP 类报错,同时 std 原生注解类不经过该校验。 Co-authored-by: selden --- .../reflection/reflection_attribute_new_instance.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/std/php/reflection/reflection_attribute_new_instance.go b/std/php/reflection/reflection_attribute_new_instance.go index 2ffb7f14..4ad14739 100644 --- a/std/php/reflection/reflection_attribute_new_instance.go +++ b/std/php/reflection/reflection_attribute_new_instance.go @@ -51,8 +51,10 @@ func (m *ReflectionAttributeNewInstanceMethod) Call(ctx data.Context) (data.GetV } if object, ok := ctx.(*data.ClassMethodContext); ok { if metadata, ok := object.Class.(*ReflectionAttributeClass); ok && metadata.target != 0 { - flags := -1 + // 仅对用户定义的 PHP 类(*ClassStatement)执行 #[Attribute] 校验; + // std 原生注解类实现 data.ClassStmt 但无 PHP 类体,校验放在外层跳过。 if declared, ok := class.(*node.ClassStatement); ok { + flags := -1 for _, marker := range declared.Annotations { if data.TypeNameEqual(marker.Class.GetName(), "Attribute") { if value, ctl := marker.GetProperty("flags"); ctl == nil { @@ -62,10 +64,9 @@ func (m *ReflectionAttributeNewInstanceMethod) Call(ctx data.Context) (data.GetV } } } - } - // 仅有 PHP 类上的 #[Attribute] 元数据时才做目标/可重复性校验; - // std 原生注解类无该元数据,但已在运行时成功挂载,允许 newInstance。 - if flags >= 0 { + if flags < 0 { + return nil, data.NewErrorThrowByName(nil, errors.New("Attempting to use non-attribute class "+annotation.Name+" as attribute"), "Error") + } if flags&metadata.target == 0 { return nil, data.NewErrorThrowByName(nil, errors.New("Attribute "+annotation.Name+" cannot target function"), "Error") } From faf3d1f4a15b64d4f059cb2acf16e222cf968b89 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Nov 2026 13:55:18 +0000 Subject: [PATCH 3/3] =?UTF-8?q?fix(reflection):=20Container=20newInstance?= =?UTF-8?q?=20=E4=B8=8E=20TARGET=5FPARAMETER=20=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 带 target 形参的注解经 InstantiateAnnotationForReflection 绑定实参且不执行构造副作用; 修正 ReflectionAttribute 目标常量(PARAMETER=32);补充 Container/参数 target 回归测试。 Co-authored-by: selden --- node/annotation.go | 90 +------------ node/annotation_reflection.go | 121 ++++++++++++++++++ std/php/reflection/annotation_bind.go | 19 +++ .../reflection_attribute_factory.go | 11 +- .../reflection_attribute_new_instance.go | 14 +- tests/php/reflection_attribute_args_test.php | 112 ++++++++++++++++ 6 files changed, 271 insertions(+), 96 deletions(-) create mode 100644 node/annotation_reflection.go diff --git a/node/annotation.go b/node/annotation.go index 525dde1a..cfbe9859 100644 --- a/node/annotation.go +++ b/node/annotation.go @@ -1,8 +1,6 @@ package node import ( - "fmt" - "github.com/php-any/origami/data" ) @@ -107,92 +105,10 @@ func (a *Annotation) GetValue(ctx data.Context) (data.GetValue, data.Control) { if object, ok := object.(*data.ClassValue); ok { if method := object.Class.GetConstruct(); method != nil { - varies := method.GetVariables() - params := method.GetParams() - fnCtx := object.CreateContext(varies) - // 入参的值设置到上下文中 - for index, arg := range a.Arguments { - switch argTV := arg.(type) { - case *NamedArgument: - tempV, acl := argTV.GetValue(ctx) - if acl != nil { - return nil, acl - } - vari, err := findVariable(varies, argTV.Name) - if err != nil { - return nil, data.NewErrorThrow(a.from, err) - } - fnCtx.SetVariableValue(vari, tempV.(data.Value)) - default: - tempV, acl := argTV.GetValue(ctx) - if acl != nil { - return nil, acl - } - - if index >= len(varies) { - return nil, data.NewErrorThrow(a.from, fmt.Errorf("注解(%v)构造函数参数数量超出限制: %d", a.Name, index)) - } - - fnCtx.SetVariableValue(varies[index], tempV.(data.Value)) - } - } - - // 处理未传递的参数,设置默认值 - for index := len(a.Arguments); index < len(params); index++ { - if index >= len(varies) { - break - } - param := params[index] - if targetParam := asAnnotationTargetParam(param); targetParam != nil { - // 将被注解的 AST 目标按需注入构造函数 - // 注意:a.Target 类型是 data.GetValue,直接塞进 any 会二次装箱导致 - // anyT.Value.(*ClassStatement) 失败,需先解开具体类型。 - fnCtx.SetVariableValue(targetParam, data.NewAnyValue(unwrapAnnotationTarget(a.Target))) - continue - } - if argObj, ok := param.(*Parameter); ok { - if argObj.DefaultValue == nil { - return nil, data.NewErrorThrow(a.from, fmt.Errorf("调用 %s 构造函数时参数 %s 缺少值和默认值", a.Name, argObj.Name)) - } - // 调用 GetValue 来触发默认值的设置 - _, acl := argObj.GetValue(fnCtx) - if acl != nil { - return nil, acl - } - } + fnCtx, acl := bindAnnotationConstructorArgs(a, ctx, object) + if acl != nil { + return nil, acl } - - // 将构造函数参数属性的值赋值给对象属性(PHP 8 构造函数参数属性提升) - for index, param := range params { - // 检查是否是属性提升的参数 - if promotedParam, ok := param.(*PromotedParameter); ok { - // 从函数上下文获取参数值 - if index < len(varies) { - paramValue, acl := fnCtx.GetVariableValue(varies[index]) - if acl != nil { - // 如果获取失败,尝试使用默认值 - if promotedParam.DefaultValue != nil { - paramValueGet, acl := promotedParam.DefaultValue.GetValue(fnCtx) - if acl != nil { - return nil, acl - } - if paramValueGet != nil { - paramValue = paramValueGet.(data.Value) - } - } else { - // 没有默认值,跳过 - continue - } - } - // 将参数值赋值给对象属性 - if paramValue != nil { - object.SetProperty(promotedParam.PropertyName, paramValue) - } - } - } - } - - // 构造函数执行成功后,返回注解实例本身 return object, &CallAnn{method: method, ctx: fnCtx} } } diff --git a/node/annotation_reflection.go b/node/annotation_reflection.go new file mode 100644 index 00000000..11270131 --- /dev/null +++ b/node/annotation_reflection.go @@ -0,0 +1,121 @@ +package node + +import ( + "fmt" + + "github.com/php-any/origami/data" +) + +// ClassStmtHasAnnotationTargetParameter 注解构造函数是否包含编译器注入的 target 形参。 +func ClassStmtHasAnnotationTargetParameter(stmt data.ClassStmt) bool { + if stmt == nil { + return false + } + construct := stmt.GetConstruct() + if construct == nil { + return false + } + for _, param := range construct.GetParams() { + if asAnnotationTargetParam(param) != nil { + return true + } + } + return false +} + +// InstantiateAnnotationForReflection 按注解声明绑定用户实参与 target,但不执行构造副作用(无 InitAnnotation)。 +func InstantiateAnnotationForReflection(a *Annotation, ctx data.Context) (data.GetValue, data.Control) { + stmt, acl := a.resolveClass(ctx) + if acl != nil { + return nil, acl + } + object, acl := stmt.GetValue(ctx.CreateBaseContext()) + if acl != nil { + return nil, acl + } + cv, ok := object.(*data.ClassValue) + if !ok || cv.Class.GetConstruct() == nil { + return object, nil + } + if _, acl := bindAnnotationConstructorArgs(a, ctx, cv); acl != nil { + return nil, acl + } + return cv, nil +} + +// bindAnnotationConstructorArgs 将声明中的用户实参与 target 绑定到构造上下文(不调用 __construct)。 +func bindAnnotationConstructorArgs(a *Annotation, ctx data.Context, object *data.ClassValue) (data.Context, data.Control) { + method := object.Class.GetConstruct() + varies := method.GetVariables() + params := method.GetParams() + fnCtx := object.CreateContext(varies) + + for index, arg := range a.Arguments { + switch argTV := arg.(type) { + case *NamedArgument: + tempV, acl := argTV.GetValue(ctx) + if acl != nil { + return nil, acl + } + vari, err := findVariable(varies, argTV.Name) + if err != nil { + return nil, data.NewErrorThrow(a.from, err) + } + fnCtx.SetVariableValue(vari, tempV.(data.Value)) + default: + tempV, acl := argTV.GetValue(ctx) + if acl != nil { + return nil, acl + } + if index >= len(varies) { + return nil, data.NewErrorThrow(a.from, fmt.Errorf("注解(%v)构造函数参数数量超出限制: %d", a.Name, index)) + } + fnCtx.SetVariableValue(varies[index], tempV.(data.Value)) + } + } + + for index := len(a.Arguments); index < len(params); index++ { + if index >= len(varies) { + break + } + param := params[index] + if targetParam := asAnnotationTargetParam(param); targetParam != nil { + fnCtx.SetVariableValue(targetParam, data.NewAnyValue(unwrapAnnotationTarget(a.Target))) + continue + } + if argObj, ok := param.(*Parameter); ok { + if argObj.DefaultValue == nil { + return nil, data.NewErrorThrow(a.from, fmt.Errorf("调用 %s 构造函数时参数 %s 缺少值和默认值", a.Name, argObj.Name)) + } + _, acl := argObj.GetValue(fnCtx) + if acl != nil { + return nil, acl + } + } + } + + for index, param := range params { + if promotedParam, ok := param.(*PromotedParameter); ok { + if index < len(varies) { + paramValue, acl := fnCtx.GetVariableValue(varies[index]) + if acl != nil { + if promotedParam.DefaultValue != nil { + paramValueGet, acl := promotedParam.DefaultValue.GetValue(fnCtx) + if acl != nil { + return nil, acl + } + if paramValueGet != nil { + paramValue = paramValueGet.(data.Value) + } + } else { + continue + } + } + if paramValue != nil { + object.SetProperty(promotedParam.PropertyName, paramValue) + } + } + } + } + return fnCtx, nil +} diff --git a/std/php/reflection/annotation_bind.go b/std/php/reflection/annotation_bind.go index df4c0b34..6ac1de80 100644 --- a/std/php/reflection/annotation_bind.go +++ b/std/php/reflection/annotation_bind.go @@ -70,6 +70,25 @@ func bindUserAnnotationProperties(ctx data.Context, inst *data.ClassValue, decl } } +func attributeTargetMismatchMessage(attributeName string, target int) string { + switch target { + case reflectionAttributeTargetClass: + return "Attribute " + attributeName + " cannot target class" + case reflectionAttributeTargetFunction: + return "Attribute " + attributeName + " cannot target function" + case reflectionAttributeTargetMethod: + return "Attribute " + attributeName + " cannot target method" + case reflectionAttributeTargetProperty: + return "Attribute " + attributeName + " cannot target property" + case reflectionAttributeTargetClassConstant: + return "Attribute " + attributeName + " cannot target class constant" + case reflectionAttributeTargetParameter: + return "Attribute " + attributeName + " cannot target parameter" + default: + return "Attribute " + attributeName + " cannot target this declaration" + } +} + func constructorParamPropertyName(param data.GetValue) string { switch p := param.(type) { case *node.PromotedParameter: diff --git a/std/php/reflection/reflection_attribute_factory.go b/std/php/reflection/reflection_attribute_factory.go index e97d7eb1..f93035a4 100644 --- a/std/php/reflection/reflection_attribute_factory.go +++ b/std/php/reflection/reflection_attribute_factory.go @@ -8,11 +8,12 @@ import ( ) const ( - reflectionAttributeTargetClass = 1 - reflectionAttributeTargetFunction = 2 - reflectionAttributeTargetMethod = 4 - reflectionAttributeTargetProperty = 8 - reflectionAttributeTargetParameter = 16 + reflectionAttributeTargetClass = 1 + reflectionAttributeTargetFunction = 2 + reflectionAttributeTargetMethod = 4 + reflectionAttributeTargetProperty = 8 + reflectionAttributeTargetClassConstant = 16 + reflectionAttributeTargetParameter = 32 ) // newReflectionAttribute 创建 ReflectionAttribute 实例;decl 为源码注解节点(可为 nil)。 diff --git a/std/php/reflection/reflection_attribute_new_instance.go b/std/php/reflection/reflection_attribute_new_instance.go index 4ad14739..defc7f0a 100644 --- a/std/php/reflection/reflection_attribute_new_instance.go +++ b/std/php/reflection/reflection_attribute_new_instance.go @@ -68,7 +68,7 @@ func (m *ReflectionAttributeNewInstanceMethod) Call(ctx data.Context) (data.GetV return nil, data.NewErrorThrowByName(nil, errors.New("Attempting to use non-attribute class "+annotation.Name+" as attribute"), "Error") } if flags&metadata.target == 0 { - return nil, data.NewErrorThrowByName(nil, errors.New("Attribute "+annotation.Name+" cannot target function"), "Error") + return nil, data.NewErrorThrowByName(nil, errors.New(attributeTargetMismatchMessage(annotation.Name, metadata.target)), "Error") } if metadata.repeated && flags&64 == 0 { return nil, data.NewErrorThrowByName(nil, errors.New("Attribute "+annotation.Name+" must not be repeated"), "Error") @@ -76,9 +76,15 @@ func (m *ReflectionAttributeNewInstanceMethod) Call(ctx data.Context) (data.GetV } } } - instance, ctl := node.CreateInstanceFromClass(class, annotation.Arguments, ctx) - if ctl != nil { - return nil, ctl + var instance data.GetValue + var instCtl data.Control + if node.ClassStmtHasAnnotationTargetParameter(class) { + instance, instCtl = node.InstantiateAnnotationForReflection(annotation, ctx) + } else { + instance, instCtl = node.CreateInstanceFromClass(class, annotation.Arguments, ctx) + } + if instCtl != nil { + return nil, instCtl } if cv, ok := instance.(*data.ClassValue); ok { bindUserAnnotationProperties(ctx, cv, annotation) diff --git a/tests/php/reflection_attribute_args_test.php b/tests/php/reflection_attribute_args_test.php index 6c9be5fe..7946d518 100644 --- a/tests/php/reflection_attribute_args_test.php +++ b/tests/php/reflection_attribute_args_test.php @@ -2,6 +2,10 @@ namespace tests\php; +use Container\Annotation\Bind; +use Container\Annotation\Inject; +use Container\Annotation\Named; +use Container\Annotation\Singleton; use Database\Annotation\Column; use Database\Annotation\Table; use Net\Annotation\GetMapping; @@ -54,6 +58,55 @@ class ReflectionAttr_MixedArgs { } +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)] +class ReflectionAttr_InjectConfig +{ + public function __construct(public string $key) + { + } +} + +#[Attribute(Attribute::TARGET_FUNCTION)] +class ReflectionAttr_FunctionOnly +{ + public function __construct() + { + } +} + +#[Singleton(name: 'loom.module')] +class ReflectionAttr_SingletonHolder +{ +} + +interface ReflectionAttr_BindContract +{ +} + +#[Bind(abstract: 'tests\\php\\ReflectionAttr_BindContract')] +class ReflectionAttr_BindHolder +{ +} + +class ReflectionAttr_ContainerCtor +{ + public function __construct( + #[Inject(service: 'db.main')] + #[Named(name: 'primary')] + mixed $conn = null, + ) { + } +} + +class ReflectionAttr_ParamTargets +{ + public function __construct( + #[ReflectionAttr_InjectConfig('app.cache')] + mixed $cache = null, + ) { + } +} + function assertArgsNonEmpty($attr, string $key, $expected): void { $args = $attr->getArguments(); @@ -125,6 +178,65 @@ function assertArgsNonEmpty($attr, string $key, $expected): void \Log::fatal('Command newInstance 属性错误'); } +// --- Container:Singleton / Bind / Inject / Named newInstance 不得抛 target 类型错误 --- +$singletonRef = new \ReflectionClass(ReflectionAttr_SingletonHolder::class); +$singletonAttr = $singletonRef->getAttributes(Singleton::class)[0]; +assertArgsNonEmpty($singletonAttr, 'name', 'loom.module'); +$singletonInst = $singletonAttr->newInstance(); +if (($singletonInst->name ?? '') !== 'loom.module') { + \Log::fatal('Singleton newInstance()->name 错误'); +} + +$bindRef = new \ReflectionClass(ReflectionAttr_BindHolder::class); +$bindAttr = $bindRef->getAttributes(Bind::class)[0]; +assertArgsNonEmpty($bindAttr, 'abstract', 'tests\\php\\ReflectionAttr_BindContract'); +$bindInst = $bindAttr->newInstance(); +if (($bindInst->abstract ?? '') !== 'tests\\php\\ReflectionAttr_BindContract') { + \Log::fatal('Bind newInstance()->abstract 错误'); +} + +$ctorRef = new \ReflectionClass(ReflectionAttr_ContainerCtor::class)->getConstructor(); +$ctorParam = $ctorRef->getParameters()[0]; +$injectAttr = $ctorParam->getAttributes(Inject::class)[0]; +$injectInst = $injectAttr->newInstance(); +if (($injectInst->service ?? '') !== 'db.main') { + \Log::fatal('Inject newInstance()->service 错误'); +} +$namedAttr = $ctorParam->getAttributes(Named::class)[0]; +$namedInst = $namedAttr->newInstance(); +if (($namedInst->name ?? '') !== 'primary') { + \Log::fatal('Named newInstance()->name 错误'); +} + +// --- 参数属性 target:PROPERTY|PARAMETER 合法;仅 FUNCTION 的注解在参数上应失败 --- +$paramRef = new \ReflectionClass(ReflectionAttr_ParamTargets::class)->getConstructor()->getParameters()[0]; +$configAttr = $paramRef->getAttributes(ReflectionAttr_InjectConfig::class)[0]; +$configArgs = $configAttr->getArguments(); +$configKey = $configArgs['key'] ?? ($configArgs[0] ?? null); +if ($configKey !== 'app.cache') { + \Log::fatal('InjectConfig getArguments key 错误: ' . var_export($configArgs, true)); +} +$configInst = $configAttr->newInstance(); +if ($configInst->key !== 'app.cache') { + \Log::fatal('InjectConfig newInstance()->key 错误'); +} + +class ReflectionAttr_FunctionOnlyParam +{ + public function __construct( + #[ReflectionAttr_FunctionOnly] + mixed $x = null, + ) { + } +} +$badParam = (new \ReflectionClass(ReflectionAttr_FunctionOnlyParam::class))->getConstructor()->getParameters()[0]; +$badAttr = $badParam->getAttributes(ReflectionAttr_FunctionOnly::class)[0]; +try { + $badAttr->newInstance(); + \Log::fatal('仅 TARGET_FUNCTION 的注解在参数上 newInstance 应抛 Error'); +} catch (\Error $e) { +} + // --- 用户自定义属性:混用位置参数与命名参数 --- $mixedRef = new \ReflectionClass(ReflectionAttr_MixedArgs::class); $customAttrs = $mixedRef->getAttributes(ReflectionAttr_CustomMark::class);