On Thu, 3 Jul 2025 at 00:26, Andreas Hennings <andr...@dqxtech.net> wrote:
>
> 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


I created an RFC and PR with _only_ the
ReflectionAttribute->getTargetReflector()
I still get memory leaks, says the pipeline.
https://wiki.php.net/rfc/attribute-target-reflector
https://github.com/php/php-src/pull/19066
For now this shall be considered draft, but whoever is interested may
have a look.

-- Andreas

Reply via email to