crell/attributeutils

一个强大、灵活的属性处理框架

资助包维护!
Crell

1.2.0 2024-07-11 17:15 UTC

This package is auto-updated.

Last update: 2024-09-11 17:40:44 UTC


README

Latest Version on Packagist Software License Total Downloads

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();
    }
}

绝不应该将反射对象本身保存到属性对象中。反射对象不能被缓存,保存它将使属性对象无法缓存。这也很浪费,因为您所需的所有数据都可以从反射对象中检索并单独保存。

类似地,还有用于处理类相应部分的接口,例如:FromReflectionPropertyFromReflectionMethodFromReflectionClassConstantFromReflectionParameter

额外的类组件

类属性还可以选择分析类的各个部分,如属性、方法和常量。它通过实现ParsePropertiesParseStaticPropertiesParseMethodsParseStaticMethodsParseClassConstants接口来实现。它们的工作方式相同,所以我们以属性为例。

举例说明是最简单的方法

#[\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()方法返回OneTwo的属性。如果有多个属性在同一组件上符合该规则(比如说,一个返回['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类来查找属性,并在那里找到它。

如果一个属性同时实现了 InheritableTransitive,那么首先检查正在分析的类,然后是其祖先类,接着是其实现的接口,然后是它类型化传递的类,最后是那个类的祖先类,直到找到合适的属性。

主属性和子属性都可以声明为 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);

子属性、ParseParametersFinalizable 以及作用域都在函数上按与类和方法完全相同的方式工作。还有一个相应的 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

IteratorAggregateArrayAccess 接口是可选的;我在这里只是想表明,如果你想这么做,你完全可以。在这里,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
{
    // ...
}

你现在可以在同一个类上混合使用 RealNameAlias。只允许一个 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);

在这种情况下,一个类可以被标记为 ScreenAudio,但不能同时标记。如果都指定了,则只使用第一个列出的,其他的将被忽略。

在这个例子中,$displayInfoA->type 将是 Screen 的实例,$displayInfoB->type 将是 Audio 的实例,而 $displayInfoC->type 将是 null

变更日志

请参阅 CHANGELOG 了解最近的变化信息。

测试

$ composer test

贡献

请参阅 CONTRIBUTINGCODE_OF_CONDUCT 了解详细信息。

安全

如果您发现任何与安全相关的问题,请使用 GitHub 安全报告表单 而不是问题队列。

鸣谢

本库的初始开发由 TYPO3 GmbH 赞助。

许可证

Lesser GPL 版本 3 或更高版本。请参阅 许可证文件 了解更多信息。