This topic was discussed in the past as "Declaration-aware attributes", and mentioned in the discussion to "Amendments to Attributes". I now want to propose a close-to-RFC iteration of this. (I don't have RFC Karma, my wiki account is "Andreas Hennings (donquixote)")
----- Primary proposal ============= I propose to introduce 3 new methods on ReflectionAttribute. static ReflectionAttribute::getCurrentTargetReflector(): ?Reflector Most of the time, this will return NULL. During the execution of ReflectionAttribute->newInstance(), it will return the reflector of the symbol on which the attribute is found. (in other words, during $reflector->getAttributes()[$i]->newInstance(), it will return $reflector.) During the execution of ReflectionAttribute::invokeWithTargetAttribute($target, $callback), it will return $target. If the call stack contains multiple calls to the above mentioned methods, only the closest/deepest one counts. (This means that php needs to maintain a stack of reflectors.) static ReflectionAttribute::invokeWithTargetReflector(?Reflector $target, callable $callback): void This will invoke $callback, with no arguments. During the invocation, ReflectionAttribute::getCurrentTargetReflector() will return $target. (This allows testing attribute classes without using them as attributes.) ReflectionAttribute->getTargetReflector(): \Reflector This returns the reflector of the symbol on which the attribute is found. This method mostly exists for completeness: The ReflectionAttribute must store the target reflector, so one would expect to be able to obtain it. Example #[Attribute(Attribute::TARGET_PARAMETER)] class MyAutowireAttribute { public readonly string $serviceId; public function __construct() { $reflectionParameter = ReflectionAttribute::getCurrentTargetReflector(); if ($reflectionParameter === null) { throw new \RuntimeException('This class can only be instantiated as an attribute.'); } assert($reflectionParameter instanceof ReflectionParameter); // @todo Some validation. $this->serviceId = (string) $reflectionParameter->getType(); } } class MyService { public function __construct(#[MyAutowireAttribute] private readonly MyOtherService $otherService) {} } // Regular usage. $reflector = (new ReflectionMethod(MyService::class, '__construct'))->getParameters()[0]; $reflection_attribute = $reflector->getAttributes()[0]; assert($reflection_attribute->getTargetReflector() === $reflector); $attribute = $reflection_attribute->newInstance(); assert($attribute instanceof MyAutowireAttribute); assert($attribute->serviceId === MyOtherService::class); // Simulation mode for tests. $reflector = (new ReflectionFunction(fn (MyOtherService $arg) => null))->getParameters()[0]; $attribute = ReflectionAttribute::invokeWithTargetReflector($reflector, fn () => new MyAutowireAttribute()); assert($attribute instanceof MyAutowireAttribute); assert($attribute->serviceId === MyOtherService::class); // Nested calls. function test(\Reflector $a, \Reflector $b) { assert(ReflectionAttribute::getCurrentTargetReflector() === null); ReflectionAttribute::invokeWithTargetReflector($a, function () use ($a, $b) { assert(ReflectionAttribute::getCurrentTargetReflector() === $a); ReflectionAttribute::invokeWithTargetReflector($b, function () use ($b) { assert(ReflectionAttribute::getCurrentTargetReflector() === $b); ReflectionAttribute::invokeWithTargetReflector(null, function () { assert(ReflectionAttribute::getCurrentTargetReflector() === null); }); }); assert(ReflectionAttribute::getCurrentTargetReflector() === $a); }); assert(ReflectionAttribute::getCurrentTargetReflector() === null); } ------------------------------ Alternative proposal ================= For completeness, I am also proposing an alternative version of this. The two are not necessarily mutually exclusive, but having both would introduce some kind of redundancy. Personally, I prefer the first proposal (see below why). I propose to introduce 3 new methods on ReflectionAttribute. static ReflectionAttribute::getCurrent(): ?\ReflectionAttribute Most of the time, this will return NULL. During the execution of ReflectionAttribute->newInstance(), it will return the ReflectionAttribute instance on which ->newInstance() was called. ReflectionAttribute->getTargetReflector(): \Reflector This returns the reflector of the symbol on which the attribute is found. static ReflectionAttribute::create(\Reflector $target, string $name, array $arguments, bool $is_repeated = false): \ReflectionAttribute This returns a ReflectionAttribute object that behaves as if the attribute was found on $target. This is mostly for testing purposes. Example #[Attribute(Attribute::TARGET_PARAMETER)] class MyAutowireAttribute { public readonly string $serviceId; public function __construct() { $reflectionParameter = ReflectionAttribute::getCurrent()->getTargetReflector(); [..] // @todo Some validation. $this->serviceId = (string) $reflectionParameter->getType(); } } class MyService { public function __construct(#[MyAutowireAttribute] private readonly MyOtherService $otherService) {} } // Regular usage. $reflection_parameter = (new ReflectionMethod(MyService::class, '__construct'))->getParameters()[0]; $reflection_attribute = $reflection_parameter->getAttributes()[0]; assert($reflection_attribute->getTargetReflector() === $reflection_parameter); $attribute_instance = $reflectionAttribute->newInstance(); assert($attribute_instance instanceof MyAutowireAttribute); assert($attribute_instance->serviceId === MyOtherService::class); // Simulation mode for tests. $reflection_parameter = (new ReflectionFunction(fn (MyOtherService $arg) => null))->getParameters()[0]; $reflection_attribute = ReflectionAttribute::create($reflection_parameter, MyAutowireAttribute::class, []); assert($reflection_attribute->getTargetReflector() === $reflection_parameter); assert($reflection_attribute->getTargetReflector()->getAttributes() === []); $attribute_instance = $reflection_attribute->newInstance(); assert($attribute_instance instanceof MyAutowireAttribute); assert($attribute_instance->serviceId === MyOtherService::class); Why do I like this version less? For most use cases, the attribute instance does not need access to the ReflectionAttribute object. For the testing scenario, the "fake" ReflectionAttribute object feels strange, because: - ReflectionAttribute::create($reflector, ...)->getTargetReflector()->getAttributes() may be empty, or does not contain the fake attribute. - ReflectionAttribute::create($reflector, ...)->isRepeated() is completely meaningless. - If we add ReflectionAttribute->getPosition() in the future, the result from the "fake" one will be off. Any code that relies on these methods of ReflectionAttribute to look for other attributes on the same symbol may break with a "fake" instance. Details, thoughts ================= The return type for ReflectionAttribute::getCurrentTargetReflector() would not simply be "Reflector", but "\ReflectionClass|\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty|\ReflectionClassConstant", assuming that no dedicated interface is introduced until then. For ReflectionAttribute::getCurrentTargetReflector(), I was wondering if instead we may want a function like current_attribute_target(). This would be inspired by func_get_args(). In the end, the method is still related to reflection, so for now I decided to keep it here. For ReflectionAttribute::invokeWithTargetReflector(), we could instead introduce something with ::push() and ::pop(). This would be more flexible, but it would also lead to people forgetting to remove a reflector that was set temporarily, leaving the system polluted. For ReflectionAttribute::invokeWithTargetReflector() returning NULL, we could instead have it throw an exception. But then people might want an alternative method or mode that _does_ return NULL when called outside ->newInstance(). By having it return NULL, the calling code can decide whether and which exception to throw. Implementation =============== An instance of ReflectionAttribute would need to maintain a reference to the reflector it was created from. The ReflectionAttribute class would need an internal static property with a stack of ReflectionAttribute instances, OR of Reflector instances, depending which version of the proposal is chosen. Other alternatives ====================== In older discussions, it was suggested to provide the target reflector as a special constructor parameter. This is problematic because an attribute expression #[MyAttribute('a', 'b', 'c')] expects to pass values to all the parameters. Another idea was to provide the target reflector through a kind of setter method on the attribute class. This can work, but it makes attribute classes harder to write, because the constructor does not have all the information. It may also prevent attribute classes from being stateless (depending how we define stateless). Userland implementations ========================= One userland implementation that was mentioned in this list in the past is in the 'crell/attributeutils' package. This one uses a kind of setter injection for the target reflector. See https://github.com/Crell/AttributeUtils/blob/master/src/FromReflectionClass.php Another userland implementation is in the 'ock/reflector-aware-attributes' package. https://github.com/ock-php/reflector-aware-attributes (I created that one) This supports both a setter method and getting the target reflector from the attribute constructor. The problem with any userland implementation is that it only works if the attribute is instantiated (or processed) using that userland library. Simply calling $reflector->getAttributes()[0]->newInstance() would either return an instance that is incomplete, or it would break, if the attribute class expects access to its target. -------- I can create an RFC, if I get the Karma :) But, perhaps we want to discuss a bit first. -- Andreas