Userland attributes (i.e. attributes not provided by PHP itself or an extension) are created with ReflectionAttribute::newInstance() and have their constructors called with the arguments provided to the attribute. However, they lack addition context as to what they are being applied to. While it is possible to hack around with debug_backtrace to identify the original ReflectionAttribute instance, and thus the type of construct the attribute is applied to (class, class constant, etc.) there is no way to identify which class (or class constant, etc.) the attribute is being applied to.
This RFC proposes the addition of a new static method, ReflectionAttribute::getCurrent(), that, when called from an attribute constructor, returns a reflection object corresponding to what the attribute was applied to.
For the purposes of this RFC, an “item” is something that can have attributes applied to it, including classes, functions, parameters, constants, etc.
Attributes were first introduced in attributes_v2 in PHP 8.0, with the current syntax adopted shorter_attribute_syntax_change. Userland attributes provide a way to add metadata to items in a structured way that can be read by external tools using reflection.
However, userland attributes, when created via ReflectionAttribute::newInstance(), only know about the arguments passed to the attribute when it was applied. Backtracing hacking can be used to identify the ReflectionAttribute object that was used, and from there ReflectionAttribute::getTarget() could be used to identify what type of item the attribute was applied to, but not which specific item.
Various workarounds exist in userland, such as:
FromReflectionClass) that indicate an attribute wants to be told what item (e.g. for FromReflectionClass, what class) the attribute was applied to. This information is provided after the attribute is constructed, and only when the library.::tryFrom static constructors to use (e.g. Symfony/console's attributes like Option)
However, these rely on using an external library or creating attributes separately from ReflectionAttribute::newInstance(). This RFC proposes a PHP-provided replacement, ReflectionAttribute::getCurrent(). This would allow attributes to add custom validation or otherwise make use of the information about what item they were applied to:
<?php #[Attribute] class Demo { public function __construct($args) { // will echo the same ReflectionClass that `echo $case;` does below // this is a contrived example, real world code would do something useful echo ReflectionAttribute::getCurrent(); } } #[Demo("class")] class WithDemo { } $case = new ReflectionClass(WithDemo::class); echo $case; echo "\n"; $case->getAttributes()[0]->newInstance(); ?>
When called from the constructor of an attribute, the method will return one of the following, depending on what item the attribute was applied to. Attributes can be applied to different types of items depending on the Attribute::TARGET_* flags, and even among the same type of item the reflection extension sometimes has more specific classes to use
| Flag | Applied to | Return type |
|---|---|---|
Attribute::TARGET_CLASS | Class | ReflectionClass |
Attribute::TARGET_CLASS | Trait | ReflectionClass |
Attribute::TARGET_CLASS | Interface | ReflectionClass |
Attribute::TARGET_CLASS | Enum | ReflectionEnum |
Attribute::TARGET_METHOD | Class method | ReflectionMethod |
Attribute::TARGET_CLASS_CONSTANT | Class constant | ReflectionClassConstant |
Attribute::TARGET_CLASS_CONSTANT | Enum case (unit) | ReflectionEnumUnitCase |
Attribute::TARGET_CLASS_CONSTANT | Enum case (backed) | ReflectionEnumBackedCase |
Attribute::TARGET_PROPERTY | Class property | ReflectionProperty |
Attribute::TARGET_FUNCTION | Global function | ReflectionFunction |
Attribute::TARGET_PARAMETER | Function parameter | ReflectionParameter |
Attribute::TARGET_PARAMETER | Method parameter | ReflectionParameter |
Attribute::TARGET_CONSTANT | Global constant | ReflectionConstant |
Note that
ReflectionEnum is a subclass of ReflectionClassReflectionEnumBackedCase is a subclass of ReflectionEnumUnitCase which is a subclass of ReflectionClassConstant
To avoid a huge union return type, and to account for potential future expansions of attribute targets (e.g. attributes-on-constants) or new reflection subclasses, the return type will be declared with a new interface, ReflectionAttributeTarget, that represents “things that can have attributes” or “things that an attribute can be applied to”.
Thus, the additions in this RFC look like the following (classes that implement Reflector did so previously, and are just updated to also implement ReflectionAttributeTarget)
<?php interface ReflectionAttributeTarget extends Reflector { public function getAttributes(?string $name = null, int $flags = 0): array {} } // ... abstract class ReflectionFunctionAbstract implements Reflector, ReflectionAttributeTarget { /* ... */ } class ReflectionClass implements Reflector, ReflectionAttributeTarget { /* ... */ } class ReflectionProperty implements Reflector, ReflectionAttributeTarget { /* ... */ } class ReflectionClassConstant implements Reflector, ReflectionAttributeTarget { /* ... */ } class ReflectionParameter implements Reflector, ReflectionAttributeTarget { /* ... */ } class ReflectionConstant implements Reflector, ReflectionAttributeTarget { /* ... */ } class ReflectionAttribute implements Reflector { // ... public static function getCurrent(): ReflectionAttributeTarget {} } ?>
Simple example:
<?php #[Attribute(Attribute::TARGET_CLASS)] class StableInterface { public function __construct() { $target = ReflectionAttribute::getCurrent(); // Only targets classes assert($target instanceof ReflectionClass); // Should only be applied to interfaces if (!$target->isInterface()) { throw new Exception("#[StableInterface] is only for interfaces!"); } // Some global function that exists elsewhere record_stable_interface($target->getName()); } } ?>
The above attribute, when instantiated, will confirm that it is being applied to an interface (rather than a class, trait, or enum) and then record the name of the targeted interface. This could then be used by tools to automatically generate a list of interfaces in a library that are considered stable, perhaps with the help of the olvlvl/composer-attribute-collector library.
ReflectionAttribute::getCurrent() must be called directly from the constructor of an attribute that is being instantiated with ReflectionAttribute::newInstance(), otherwise it fails.
None
Next minor version (PHP 8.6).
Attribute-related libraries may want to make use of the new feature.
Only ext/reflection is affected - all of this is implemented in reflection.
None
Make sure there are no open issues when the vote starts!
When ReflectionAttribute::getCurrent() is not called from the constructor of an attribute being instantiated via ReflectionAttribute::newInstance(), should it throw an exception, or return null?
Now that ReflectionAttribute knows what it is being applied to, maybe there would be a way to retrieve that information directly from an instance of ReflectionAttribute
Add ReflectionAttribute::getCurrent() and the associated ReflectionAttributeTarget interface?
Primary Vote requiring a 2/3 majority to accept the RFC:
After the RFC is implemented, this section should contain:
Keep this updated with features that were discussed on the mail lists.
If there are major changes to the initial proposal, please include a short summary with a date or a link to the mailing list announcement here, as not everyone has access to the wikis' version history.