crell / attributeutils
一个强大、灵活的属性处理框架
Requires
- php: ~8.1
- crell/fp: ~1.0.0
Requires (Dev)
- phpbench/phpbench: ^1.2
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ~10.3
- psr/cache: ^3.0
- psr/cache-util: ^2.0
Suggests
- psr/cache: Caching analyzer rests is recommended, and a bridge for psr/cache is included.
README
AttributeUtils为PHP 8.1及更高版本提供了简化属性操作和读取的实用工具。
其主要工具是类分析器,允许您针对某些属性类分析给定的类或枚举。属性类可以通过实现各种接口来选择加入额外行为,具体描述如下。整体目标是提供一个简单但强大的框架,用于从类中读取元数据,包括反射数据。
安装
通过Composer
$ composer require crell/attributeutils
用法
基本用法
系统中最重要的类是Analyzer
,它实现了ClassAnalyzer
接口。
#[MyAttribute(a: 1, b: 2)] class Point { public int $x; public int $y; public int $z; } $analyzer = new Crell\AttributeUtils\Analyzer(); $attrib = $analyzer->analyze(Point::class, MyAttribute::class); // $attrib is now an instance of MyAttribute. print $attrib->a . PHP_EOL; // Prints 1 print $attrib->b . PHP_EOL; // Prints 2
所有与反射系统的交互都由Analyzer
抽象化。
您可以针对任何属性分析任何类。如果找不到属性,将创建一个没有任何参数的属性类新实例,即使用其默认参数值。如果需要任何参数,将抛出RequiredAttributeArgumentsMissing
异常。
最终结果是,您可以使用任何属性类分析类,只要它没有必需的参数。
然而,Analyzer
最重要的部分是它允许属性选择加入额外行为,成为一个完整的类分析和反射框架。
反射
如果一个类属性实现了Crell\AttributeUtils\FromReflectionClass
,那么一旦属性被实例化,将被分析的类的ReflectionClass
表示将传递给fromReflection()
方法。属性可以保存它需要的任何反射信息,以任何需要的方式。例如,如果您想使属性对象知道它来自哪个类,您可以保存$reflection->getName()
和/或$reflection->getShortName()
到对象的非构造函数属性上。或者,仅在未提供某些构造函数参数的情况下保存它们。
如果您直接保存反射值,强烈建议您使用与ReflectClass
属性中一致的属性名。这样,名称在所有属性中保持一致,即使在不同的库中,生成的代码也更容易被其他开发者阅读和理解。(我们稍后会更详细地介绍ReflectClass
。)
在以下示例中,属性接受一个$name
参数。如果没有提供,则使用类的短名。
#[\Attribute] class AttribWithName implements FromReflectionClass { public readonly string $name; public function __construct(?string $name = null) { if ($name) { $this->name = $name; } } public function fromReflection(\ReflectionClass $subject): void { $this->name ??= $subject->getShortName(); } }
绝不应该将反射对象本身保存到属性对象中。反射对象不能被缓存,保存它将使属性对象无法缓存。这也很浪费,因为您所需的所有数据都可以从反射对象中检索并单独保存。
类似地,还有用于处理类相应部分的接口,例如:FromReflectionProperty
、FromReflectionMethod
、FromReflectionClassConstant
和FromReflectionParameter
。
额外的类组件
类属性还可以选择分析类的各个部分,如属性、方法和常量。它通过实现ParseProperties
、ParseStaticProperties
、ParseMethods
、ParseStaticMethods
或ParseClassConstants
接口来实现。它们的工作方式相同,所以我们以属性为例。
举例说明是最简单的方法
#[\Attribute(\Attribute::TARGET_CLASS)] class MyClass implements ParseProperties { public readonly array $properties; public function propertyAttribute(): string { return MyProperty::class; } public function setProperties(array $properties): void { $this->properties = $properties; } public function includePropertiesByDefault(): bool { return true; } } #[\Attribute(\Attribute::TARGET_PROPERTY)] class MyProperty { public function __construct( public readonly string $column = '', ) {} } #[MyClass] class Something { #[MyProperty(column: 'beep')] protected property $foo; private property $bar; } $attrib = $analyzer->analyze(Something::class, MyClass::class);
在这个例子中,首先实例化MyClass
属性。它没有参数,这是可以接受的。然而,接口方法指定分析器应该解析Something
的属性相对于MyProperty
。如果一个属性没有这样的属性,它仍然应该被包含并实例化,不传递任何参数。
分析器将尽职尽责地创建一个包含两个MyProperty
实例的数组,一个用于$foo
,一个用于$bar
;前者具有column
值为beep
,后者具有默认的空字符串值。然后,该数组将传递给MyClass::setProperties()
以供MyClass
保存、解析、筛选或执行任何其他操作。
如果includePropertiesByDefault()
返回false
,则数组将只有一个值,来自$foo
。将忽略$bar
。
注意:传递给setProperties
的数组已经按属性名称索引,因此您无需自己这样做。
针对属性的属性(MyProperty
)也可以实现FromReflectionProperty
以获取传递给它的相应ReflectionProperty
,就像类属性可以一样。
分析器在ParseProperties
中仅包含对象级别的属性。如果您需要静态属性,请使用ParseStaticProperties
接口,它的工作方式完全相同。这两个接口可以同时实现。
ParseClassConstant
接口的工作方式与ParseProperties
相同。
方法
ParseMethods
的工作方式与ParseProperties
相同(并且还有一个对应的ParseStaticMethods
接口用于静态方法)。然而,一个方法指向的属性也可以实现ParseParameters
以检查该方法上的参数。ParseParameters
重复上述ParseProperties
的模式,方法名称相应更改。
类引用组件
一个组件指向的属性也可以实现ReadsClass
。如果是这样,那么在完成所有其他设置之后,将传递类的属性给fromClassAttribute()
方法。这允许属性从类继承默认值,或者根据类属性上设置的属性修改其行为。
排除值
解析类的组件时,是否包含它们取决于许多因素。各种Parse*
接口上的includePropertiesByDefault()
、includeMethodsByDefault()
等方法决定缺少属性的组件是否应使用默认值包含,或者完全排除。
如果include*()
方法返回true,仍然可以在需要时排除特定组件。该组件的属性可能实现了Excludable
接口,它有一个单一的方法,即exclude()
。
然后发生的事情是分析器将加载该类型的所有属性,然后过滤出那些从该方法返回true
的属性。这允许单个属性、方法等选择不进行解析。您可以为exclude()
使用任何逻辑,尽管最常见的方法可能是以下这样
#[\Attribute(\Attribute::TARGET_PROPERTY)] class MyProperty implements Excludable { public function __construct( public readonly bool $exclude = false, ) {} public function exclude(): bool { return $this->exclude; } } class Something { #[MyProperty(exclude: true)] private int $val; }
如果您采用这种手动方法,强烈建议您使用这里的命名约定以保持一致性。
属性继承
默认情况下,PHP中的属性不可继承。也就是说,如果类A
上有属性,而B
扩展了A
,那么询问反射B
有什么属性将找不到任何属性。有时这没关系,但有时不得不重复值会非常讨厌。
Analyzer
通过允许属性选择继承来解决这个问题。任何属性——对于类、属性、方法、常量或参数——也可以实现Inheritable
标记接口。这个接口没有方法,但它向系统发出信号,表明它应该检查父类和接口中的属性,如果找不到属性。
例如
#[\Attribute(\Attribute::TARGET_CLASS)] class MyClass implements Inheritable { public function __construct(public string $name = '') {} } #[MyClass(name: 'Jorge')] class A {} class B extends A {} $attrib = $analyzer->analyze(B::class, MyClass::class); print $attrib->name . PHP_EOL; // prints Jorge
因为MyClass
是可继承的,分析器会注意到它缺少在B
中,所以会检查类A
。所有属性组件都可以通过实现该接口而成为可继承的。
在检查继承属性时,首先检查祖先类,然后是实现的接口,顺序由class_implements()
返回。属性当然不会检查接口,因为接口不能有属性。
属性子类
在检查属性时,分析器使用反射中的instanceof
检查。这意味着指定的子类或实现接口的类仍然会被找到并包含在内。这适用于所有属性类型。
子属性
Analyzer
只能处理每个目标上的单个属性。然而,它也支持“子属性”的概念。子属性的工作方式类似于类可以选择解析属性或方法,但针对的是兄弟属性而不是子组件。这样,同一组件上的任何数量的属性都可以折叠成一个单一的属性对象。任何组件的任何属性都可以通过实现HasSubAttributes
来选择子属性。
以下示例应该会使问题更加清晰
#[\Attribute(\Attribute::TARGET_CLASS)] class MainAttrib implements HasSubAttributes { public readonly int $age; public function __construct( public readonly string name = 'none', ) {} public function subAttributes(): array { return [Age::class => 'fromAge']; } public function fromAge(?Age $sub): void { $this->age = $sub?->age ?? 0; } } #[\Attribute(\Attribute::TARGET_CLASS)] class Age { public function __construct(public readonly int $age = 0) {} } #[MainAttrib(name: 'Larry')] #[Age(21)] class A {} class B {} $attribA = $analyzer->analyze(A::class, MainAttrib::class); print "$attribA->name, $attribA->age\n"; // prints "Larry, 21" $attribB = $analyzer->analyze(B::class, MainAttrib::class); print "$attribB->name, $attribB->age\n"; // prints "none, 0"
subAttributes()
方法返回一个关联数组,其中包含属性类名称映射到要调用的方法。它们可以是字符串,或内联闭包,或对方法的闭包引用,如果需要,可以是私有的。例如
#[\Attribute(\Attribute::TARGET_CLASS)] class MainAttrib implements HasSubAttributes { public readonly int $age; public readonly string $name; public function __construct( public readonly string name = 'none', ) {} public function subAttributes(): array { return [ Age::class => $this->fromAge(...), Name::class => function (?Name $sub) { $this->name = $sub?->name ?? 'Anonymous'; } ]; } private function fromAge(?Age $sub): void { $this->age = $sub?->age ?? 0; } }
在加载MainAttrib
后,分析器将查找任何列出的子属性,然后将它们的结果传递给相应的方法。主属性可以保存整个子属性,或从中提取部分以保存,或执行其他任何操作。
属性可以有任何数量的子属性。
请注意,如果子属性缺失,则将null
传递给方法。这是为了允许子属性仅在指定时才具有必需的参数,同时使子属性本身是可选的。因此,您必须使回调方法的参数可空。
子属性也可能具有可继承
的特性。
多值子属性
默认情况下,PHP属性只能在一个目标上放置一次。然而,它们可以被标记为“可重复”,在这种情况下,多个相同的属性可以放置在同一个目标上。(类、属性、方法等。)
分析器不支持多值属性,但它支持多值子属性。如果子属性实现了Multivalue
标记接口,那么一个子属性数组将被传递给回调。
例如
#[\Attribute(\Attribute::TARGET_CLASS)] class MainAttrib implements HasSubAttributes { public readonly array $knows; public function __construct( public readonly string name = 'none', ) {} public function subAttributes(): array { return [Knows::class => 'fromKnows']; } public function fromKnows(array $knows): void { $this->knows = $knows; } } #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] class Knows implements Multivalue { public function __construct(public readonly string $name) {} } #[MainAttrib(name: 'Larry')] #[Knows('Kai')] #[Knows('Molly')] class A {} class B {}
在这种情况下,可以包含任意数量的Knows
属性,包括零个,但如果包含,则必须提供$name
参数。将调用fromKnows()
方法,并带有一个(可能是空的,如B
)的Knows
对象数组,然后可以根据需要对其进行处理。在这个例子中,对象被完整地保存,但也可以将它们合并成一个数组或用于设置其他值。
注意,如果多值子属性是可继承
的,则只有在没有本地子属性的情况下,才会检查祖先类。如果有至少一个,则将具有优先权,祖先类将被忽略。
注意:为了使用多值子属性,属性类本身必须被标记为“可重复”,如上例所示,否则PHP将生成错误。然而,这并不足以让分析器将其解析为多值。这是因为属性在实现作用域时也可能是多值的,但从分析器的角度来看仍然是单值。请参阅下面的作用域部分。
属性最终化
选择加入多个功能接口的属性可能不知道何时进行默认处理。属性设置何时“完成”可能并不明显。因此,属性类可以选择加入Finalizable
接口。如果指定,则保证它是属性上最后调用的方法。属性可以执行任何适当的最终准备,以便将对象视为“准备就绪”。
缓存
主Analyzer
类根本不做任何缓存。然而,它实现了ClassAnalyzer
接口,这使得它可以很容易地被其他提供缓存层的实现所包装。
例如,MemoryCacheAnalyzer
类提供了一个简单的包装器,该包装器将结果缓存在内存中的静态变量中。你应该几乎总是使用这个包装器来提高性能。
$analyzer = new MemoryCacheAnalyzer(new Analyzer());
还包括PSR-6缓存桥,允许分析器与任何PSR-6兼容的缓存池一起使用。
$anaylzer = new Psr6CacheAnalyzer(new Analyzer(), $somePsr6CachePoolObject);
包装器也可以组合在一起,以下将是一个完全有效且可能很好的方法
$analyzer = new MemoryCacheAnalyzer(new Psr6CacheAnalyzer(new Analyzer(), $psr6CachePool));
高级功能
还有一些其他高级功能也可用。这些功能使用频率较低,但在适当的情况下,它们非常有帮助。
作用域
属性可以选择加入支持“作用域”。作用域允许你指定在不同上下文中使用同一属性的替代版本。例如,包括不同的序列化组或不同的语言。通常,作用域会隐藏在其他库(如语言)的某个其他名称之后,这是可以的。
如果一个属性实现了SupportsScopes
,那么在查找属性时,将执行额外的过滤。具体的逻辑也与排除有关,以及类属性是否指定了一个组件应在缺少时默认加载,从而导致了高度稳健的潜在规则集,用于确定何时使用哪个属性。
例如,让我们考虑提供属性属性的备用语言版本。对于任何组件以及子属性,逻辑都是相同的。
#[\Attribute(\Attribute::TARGET_CLASS)] class Labeled implements ParseProperties { public readonly array $properties; public function setProperties(array $properties): void { $this->properties ??= $properties; } public function includePropertiesByDefault(): bool { return true; } public function propertyAttribute(): string { return Label::class; } } #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)] class Label implements SupportsScopes, Excludable { public function __construct( public readonly string $name = 'Untitled', public readonly ?string $language = null, public readonly bool $exclude = false, ) {} public function scopes(): array { return [$this->language]; } public function exclude(): bool { return $this->exclude; } } #[Labeled] class App { #[Label(name: 'Installation')] #[Label(name: 'Instalación', language: 'es')] public string $install; #[Label(name: 'Setup')] #[Label(name: 'Configurar', language: 'es')] #[Label(name: 'Einrichten', language: 'de')] public string $setup; #[Label(name: 'Einloggen', language: 'de')] #[Label(language: 'fr', exclude: true)] public string $login; public string $customization; }
类上的Labeled
属性是我们之前没有见过的。属性上的Label
属性既可排除又支持作用域,尽管它用language
这个名字暴露出来。
像之前看到的那样调用分析器将忽略作用域版本,并产生一个包含“Installation”、“Setup”、“Untitled”和“Untitled”等名称的Label
数组。然而,它也可以用一个特定的作用域来调用。
$labels = $analyzer->analyze(App::class, Labeled::class, scopes: ['es']);
现在,$labels
将包含一个包含“Instalación”、“Configurar”、“Untitled”和“Untitled”等名称的Label
数组。在$stepThree
上,没有es
作用域版本,因此回退到默认值。同样,de
作用域将导致“Installation”、“Einrichten”、“Einloggen”和“Untitled”(因为“Installation”在英语和德语中拼写相同)。
fr
作用域将导致每个情况下的默认(英语),除了$stepThree
将被完全省略。只有在这个作用域中,exclude
指令才适用。因此,结果将是“Installation”、“Setup”、“Untitled”。
(如果您真的在这样做,最好通过FromReflectionProperty
从属性名本身派生一个默认的name
,而不是使用硬编码的“Untitled”)。
相比之下,如果Labeled::includePropertiesByDefault()
返回false,则$customization
将不会包含在任何作用域中。$login
只包含在de
中,在其他任何作用域中都不包含。这是因为没有指定默认作用域选项,因此,在除de
以外的任何作用域中都不会创建默认值。对作用域fr
的查找将为空。
控制哪些属性被包含的一个有用方法是使类级别的属性作用域感知,并通过参数控制includePropertiesByDefault()
。这样,例如,includePropertiesByDefault()
可以在无作用域的情况下返回true,但在显式指定作用域时返回false;这样,属性只有在明确选择包含在该作用域中时才会被包含,而在无作用域的情况下所有属性都会被包含。
注意,scopes()
方法返回一个数组。这意味着一个属性可以是多个作用域的组成部分是完全支持的。如何填充该方法的返回值(是数组参数还是其他东西)取决于您。
此外,作用域被作为按位或的数组来查找。也就是说,以下命令
$labels = $analyzer->analyze(SomeClass::class, AnAttribute::class, scopes: ['One', 'Two']);
将检索返回任何从它们的scopes()
方法返回One
或Two
的属性。如果有多个属性在同一组件上符合该规则(比如说,一个返回['One']
,另一个返回['Two']
),则将使用字典序最先的。
传递性
传递性仅适用于属性上的属性,并且仅当所讨论的属性既可以针对属性也可以针对类时才适用。它是一种继承的替代形式。具体来说,如果一个属性类型化为一个类或接口,并且所讨论的属性实现了TransitiveProperty
,并且属性本身没有该属性,那么分析器将首先查看属性类型化的类,而不是查找继承树。
有很多条件,所以这里有一个例子来说明
#[\Attribute(\Attribute::TARGET_PROPERTY)] class MyClass implements ParseProperties { public readonly array $properties; public function setProperties(array $properties): void { $this->properties = $properties; } public function includePropertiesByDefault(): bool { return true; } public function propertyAttribute(): string { return FancyName::class; } } #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_CLASS)] class FancyName implements TransitiveProperty { public function __construct(public readonly string $name = '') {} } class Stuff { #[FancyName('A happy little integer')] protected int $foo; protected string $bar; protected Person $personOne; #[FancyName('Her Majesty Queen Elizabeth II')] protected Person $personTwo; } #[FancyName('I am not an object, I am a free man!')] class Person { } $attrib = $analyzer->analyze(Stuff::class, MyClass::class); print $attrib->properties['foo']->name . PHP_EOL; // prints "A happy little integer" print $attrib->properties['bar']->name . PHP_EOL; // prints "" print $attrib->properties['personOne']->name . PHP_EOL; // prints "I am not an object, I am a free man!" print $attrib->properties['personTwo']->name . PHP_EOL; // prints "Her Majesty Queen Elizabeth II"
因为$personTwo
有一个FancyName
属性,所以它表现正常。然而,$personOne
没有,所以它跳转到Person
类来查找属性,并在那里找到它。
如果一个属性同时实现了 Inheritable
和 Transitive
,那么首先检查正在分析的类,然后是其祖先类,接着是其实现的接口,然后是它类型化传递的类,最后是那个类的祖先类,直到找到合适的属性。
主属性和子属性都可以声明为 Transitive
。
自定义分析
作为最后的手段,属性也可以实现 CustomAnalysis
接口。如果它这样做,分析器本身将被传递给属性的 customAnalysis()
方法,然后它可以选择执行任何操作。这个功能仅作为最后的手段,如果不小心,可能会创建令人不快的无限循环。99% 的时间你应该使用其他机制。但如果有需要,它就在那里。
依赖注入
分析器被设计成可以在没有任何设置的情况下使用。通过依赖注入容器使其可用是推荐的。在 DI 配置中还应包括适当的缓存包装器。
函数分析
还有支持通过一个单独的分析器(基本上以相同的方式工作)来检索函数上的属性。《FuncAnalyzer》类实现了《FunctionAnalyzer》接口。
use Crell\AttributeUtils\FuncAnalyzer; #[MyFunc] function beep(int $a) {} $closure = #[MyClosure] fn(int $a) => $a + 1; // For functions... $analyzer = new FuncAnalyzer(); $funcDef = $analyzer->analyze('beep', MyFunc::class); // For closures $analyzer = new FuncAnalyzer(); $funcDef = $analyzer->analyze($closure, MyFunc::class);
子属性、ParseParameters
、Finalizable
以及作用域都在函数上按与类和方法完全相同的方式工作。还有一个相应的 FromReflectionFunction
接口用于接收 ReflectionFunction
对象。
FuncAnalyzer 同样也有可用的缓存包装器。它们与类分析器上的工作方式相同。
# In-memory cache. $analyzer = new MemoryCacheFunctionAnalyzer(new FuncAnalyzer()); # PSR-6 cache. $anaylzer = new Psr6CacheFunctionAnalyzer(new FuncAnalyzer(), $somePsr6CachePoolObject); # Both caches. $analyzer = new MemoryCacheFunctionAnalyzer( new Psr6CacheFunctionAnalyzer(new FuncAnalyzer(), $psr6CachePool) );
与类分析器一样,最好在您的 DI 容器中连接这些。
Reflect 库
Analyzer
的许多用途之一是从类中提取反射信息。有时你可能只需要其中的一部分,但也没有理由不能获取全部。结果是,一个属性可以携带与反射相同的所有信息,但可以在需要时进行缓存,而反射对象则不行。
在 Attributes/Reflect
目录中提供了一个此类属性的完整集合。它们涵盖了类的所有组成部分。由于它们都没有任何参数,因此没有必要将它们放在任何类上。每个默认的“空”版本都会被使用,然后它将使用 FromReflection*
接口自行填充。
最终结果是,可以通过调用以下内容获得任何任意类的完整反射摘要:
use Crell\AttributeUtls\Attributes\Reflect\ReflectClass; $reflect = $analyzer->analyze($someClass, ReflectClass::class);
$reflect
现在包含了一个完整的类、属性、常量、方法和参数反射信息的副本,在定义良好、易于缓存的对象中。请参阅每个类的 docblocks 以获取所有可用信息的完整列表。
要分析枚举,请使用 ReflectEnum::class
。
即使你不需要使用整个 Reflect 树,它也值得研究,作为如何真正利用分析器的示例。此外,如果你将任何反射值原封不动地保存到你的属性中,鼓励你使用与这些类相同的命名约定,以保持一致性。
还包括了一些特质,用于处理收集给定类组件的常见情况。如果你愿意,可以在自己的类中使用它们。
高级技巧
以下是一些高级和花哨的分析器用法,主要是为了帮助展示如果正确使用,它可以多么强大。
多值属性
正如所注,分析器在每个组件上只支持单个主属性。然而,子属性可能是多值的,省略的属性可以用默认的“空”属性填充。这导致以下模拟多值属性的方式。它适用于任何组件,但为了简单起见,我们将它在类上展示。
#[\Attribute(Attribute::TARGET_CLASS)] class Names implements HasSubAttributes, IteratorAggregate, ArrayAccess { protected readonly array $names; public function subAttributes(): array { return [Alias::class => 'fromAliases']; } public function fromAliases(array $aliases): void { $this->names = $aliases; } public function getIterator(): \ArrayIterator { return new ArrayIterator($this->names); } public function offsetExists(mixed $offset): bool { return array_key_exists($offset, $this->names); } public function offsetGet(mixed $offset): Alias { return $this->names[$offset]; } public function offsetSet(mixed $offset, mixed $value): void { throw new InvalidArgumentException(); } public function offsetUnset(mixed $offset): void { throw new InvalidArgumentException(); } } #[\Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] class Alias implements Multivalue { public function __construct( public readonly string $first, public readonly string $last, ) {} public function fullName(): string { return "$this->first $this->last"; } } #[Alias(first: 'Bruce', last: 'Wayne')] #[Alias(first: 'Bat', last: 'Man')] class Hero { // ... } $names = $analyzer->analyze(Hero::class, Names::class); foreach ($names as $name) { print $name->fullName() . PHP_EOL; } // Output: Bruce Wayne Bat Man
IteratorAggregate
和 ArrayAccess
接口是可选的;我在这里只是想表明,如果你想这么做,你完全可以。在这里,Names
属性永远不会直接放在类上。然而,通过分析一个“相对于”Names
的类,你可以收集它所有的多值子属性,给人一种多值属性的感觉。
注意,Alias
需要实现 Multivalue
,这样分析器才知道期待有多个这样的实例。
接口属性
通常,属性不会继承。这意味着接口上的属性对实现该接口的类没有影响。然而,属性可以通过分析器选择继承。
这种用法的一个好例子是子属性,它们也可以指定为接口。例如,考虑上面示例的修改版
#[\Attribute(\Attribute::TARGET_CLASS)] class Names implements HasSubAttributes, IteratorAggregate, ArrayAccess { protected readonly array $names; public function subAttributes(): array { return [Name::class => 'fromNames']; } public function fromNames(array $names): void { $this->names = $names; } // The same ArrayAccess and IteratorAggregate code as above. } interface Name extends Multivalue { public function fullName(): string; } #[\Attribute(\Attribute::TARGET_CLASS)] class RealName implements Name { public function __construct( public readonly string $first, public readonly string $last, ) {} public function fullName(): string { return "$this->first $this->last"; } } #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] class Alias implements Name { public function __construct(public readonly string $name) {} public function fullName(): string { return $this->name; } } #[RealName(first: 'Bruce', last: 'Wayne')] #[Alias('Batman')] #[Alias('The Dark Knight')] #[Alias('The Caped Crusader')] class Hero { // ... }
你现在可以在同一个类上混合使用 RealName
和 Alias
。只允许一个 RealName
,但可以有任意数量的 Alias
属性。所有这些都是根据 Names
主属性下的 Name
,因此所有这些都将被拾取并可用。
请注意,接口必须标记为 Multivalue
,这样 Analyzer
才允许有多个这种类型的属性。但是,RealName
属性没有标记为可重复的,所以 PHP 将阻止一次使用多个 RealName
,而 Alias
可以使用任意次数。
众多选项之一
类似地,可以使用子属性声明组件可以被标记为几个属性之一,但只能标记一个。
interface DisplayType {} #[\Attribute(\Attribute::TARGET_CLASS)] class Screen implements DisplayType { public function __construct(public readonly string $color) {} } #[\Attribute(\Attribute::TARGET_CLASS)] class Audio implements DisplayType { public function __construct(public readonly int $volume) {} } #[\Attribute(Attribute::TARGET_CLASS)] class DisplayInfo implements HasSubAttributes { public readonly ?DisplayType $type; public function subAttributes(): array { return [DisplayType::class => $this->fromDisplayType(...)]; } public function fromDisplayType(?DisplayType $type): void { $this->type = $type; } } #[Screen('#00AA00')] class A {} #[Audio(10)] class B {} class C {} $displayInfoA = $analyzer->analzyer(A::class, DisplayInfo::class); $displayInfoB = $analyzer->analzyer(B::class, DisplayInfo::class); $displayInfoC = $analyzer->analzyer(C::class, DisplayInfo::class);
在这种情况下,一个类可以被标记为 Screen
或 Audio
,但不能同时标记。如果都指定了,则只使用第一个列出的,其他的将被忽略。
在这个例子中,$displayInfoA->type
将是 Screen
的实例,$displayInfoB->type
将是 Audio
的实例,而 $displayInfoC->type
将是 null
。
变更日志
请参阅 CHANGELOG 了解最近的变化信息。
测试
$ composer test
贡献
请参阅 CONTRIBUTING 和 CODE_OF_CONDUCT 了解详细信息。
安全
如果您发现任何与安全相关的问题,请使用 GitHub 安全报告表单 而不是问题队列。
鸣谢
本库的初始开发由 TYPO3 GmbH 赞助。
许可证
Lesser GPL 版本 3 或更高版本。请参阅 许可证文件 了解更多信息。