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 AttributeUtils/src/FromReflectionClass.php at master · Crell/AttributeUtils · GitHub
Another userland implementation is in the
'ock/reflector-aware-attributes' package.
GitHub - ock-php/reflector-aware-attributes: Provides mechanisms for attribute objects to know about the symbol they are attached to. (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