AttributeUtils
Utilities to help ease parsing and manging of attributes
Install / Use
/learn @Crell/AttributeUtilsREADME
Attribute Utilities
[![Latest Version on Packagist][ico-version]][link-packagist] ![Software License][ico-license] [![Total Downloads][ico-downloads]][link-downloads]
AttributeUtils provides utilities to simplify working with and reading Attributes in PHP 8.1 and later.
Its primary tool is the Class Analyzer, which allows you to analyze a given class or enum with respect to some attribute class. Attribute classes may implement various interfaces in order to opt in to additional behavior, as described below. The overall intent is to provide a simple but powerful framework for reading metadata off of a class, including with reflection data.
Install
Via Composer
$ composer require crell/attributeutils
Usage
Basic usage
The most important class in the system is Analyzer, which implements the ClassAnalyzer interface.
#[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
All interaction with the reflection system is abstracted away by the Analyzer.
You may analyze any class with respect to any attribute. If the attribute is not found, a new instance of the attribute class will be created with no arguments, that is, using whatever it's default argument values are. If any arguments are required, a RequiredAttributeArgumentsMissing exception will be thrown.
The net result is that you can analyze a class with respect to any attribute class you like, as long as it has no required arguments.
The most important part of Analyzer, though, is that it lets attributes opt-in to additional behavior to become a complete class analysis and reflection framework.
Reflection
If a class attribute implements Crell\AttributeUtils\FromReflectionClass, then once the attribute has been instantiated the ReflectionClass representation of the class being analyzed will be passed to the fromReflection() method. The attribute may then save whatever reflection information it needs, however it needs. For example, if you want the attribute object to know the name of the class it came from, you can save $reflection->getName() and/or $reflection->getShortName() to non-constructor properties on the object. Or, you can save them if and only if certain constructor arguments were not provided.
If you are saving a reflection value literally, it is strongly recommended that you use a property name consistent with those in the ReflectClass attribute. That way, the names are consistent across all attributes, even different libraries, and the resulting code is easier for other developers to read and understand. (We'll cover ReflectClass more later.)
In the following example, an attribute accepts a $name argument. If one is not provided, the class's short-name will be used instead.
#[\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();
}
}
The reflection object itself should never ever be saved to the attribute object. Reflection objects cannot be cached, so saving it would render the attribute object uncacheable. It's also wasteful, as any data you need can be retrieved from the reflection object and saved individually.
There are similarly FromReflectionProperty, FromReflectionMethod, FromReflectionClassConstant, and FromReflectionParameter interfaces that do the same for their respective bits of a class.
Additional class components
The class attribute may also opt in to analyzing various portions of the class, such as its properties, methods, and constants. It does so by implementing the ParseProperties, ParseStaticProperties, ParseMethods, ParseStaticMethods, or ParseClassConstants interfaces, respectively. They all work the same way, so we'll look at properties in particular.
An example is the easiest way to explain it:
#[\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);
In this example, the MyClass attribute will first be instantiated. It has no arguments, which is fine. However, the interface methods specify that the Analyzer should then parse Something's properties with respect to MyProperty. If a property has no such attribute, it should be included anyway and instantiated with no arguments.
The Analyzer will dutifully create an array of two MyProperty instances, one for $foo and one for $bar; the former having the column value beep, and the latter having the default empty string value. That array will then be passed to MyClass::setProperties() for MyClass to save, or parse, or filter, or do whatever it wants.
If includePropertiesByDefault() returned false, then the array would have only one value, from $foo. $bar would be ignored.
Note: The array that is passed to setProperties is indexed by the name of the property already, so you do not need to do so yourself.
The property-targeting attribute (MyProperty) may also implement FromReflectionProperty to get the corresponding ReflectionProperty passed to it, just as the class attribute can.
The Analyzer includes only object level properties in ParseProperties. If you want static properties, use the ParseStaticProperties interface, which works the exact same way. Both interfaces may be implemented at the same time.
The ParseClassConstant interface works the same way as ParseProperties.
Methods
ParseMethods works the same way as ParseProperties (and also has a corresponding ParseStaticMethods interface for static methods). However, a method-targeting attribute may also itself implement ParseParameters in order to examine parameters on that method. ParseParameters repeats the same pattern as ParseProperties above, with the methods suitably renamed.
Class-referring components
A component-targeting attribute may also implement ReadsClass. If so, then the class's attribute will be passed to the fromClassAttribute() method after all other setup has been done. That allows the attribute to inherit default values from the class, or otherwise vary its behavior based on properties set on the class attribute.
Excluding values
When parsing components of a class, whether they are included depends on a number of factors. The includePropertiesByDefault(), includeMethodsByDefault(), etc. methods on the various Parse* interfaces determine whether components that lack an attribute should be included with a default value, or excluded entirely.
If the include*() method returns true, it is still possible to exclude a specific component if desired. The attribute for that component may implement the Excludable interface, with has a single method, exclude().
What then happens is the Analyzer will load all attributes of that type, then filter out the ones that return true from that method. That allows individual properties, methods, etc. to opt-out of being parsed. You may use whatever logic you wish for exclude(), although the most common approach will be something like this:
#[\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;
}
If you are taking this manual approach, it is strongly recommended that you use the naming convention here for consistency.
Attribute inheritance
By default, attributes in PHP are not inheritable. That is, if class A has an attribute on it, and B extends A, then asking reflection what attributes B has will find none. Sometimes that's OK, but other times it is highly annoying to have to repeat values.
Analyzer addresses that limitation by letting attributes opt-in to being inherited. Any attribute — for a class, property, method, constant, or parameter — may also implement the Inheritable marker interface. This interface has no methods, but signals to the system that it should itself check parent classes and interfaces for an attribute if it is not found.
For example:
#[\Attribute(\Attribute::TARGET_CLASS)]
class MyClass implements Inheritable
{
public f
