Re: [RFC] [Discussion] Add ReflectionAttribute::getCurrent()
| From: | Larry Garfield | Date: | Sat, 16 May 2026 19:20:51 +0000 |
| Subject: | Re: [RFC] [Discussion] Add ReflectionAttribute::getCurrent() | ||
| References: | 1 2 3 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-130914@lists.php.net to get a copy of this message | ||
On Sat, May 16, 2026, at 8:24 AM, Tim Düsterhus wrote:
> Hi
>
> Am 2026-05-12 22:37, schrieb Benjamin Außenhofer:
>> I am not convinced this is needed. At every call site of
>> $reflector->getAttributes() you could inject the reflector back into
>> the
>> attributes.
>
> I agree with Benjamin here and actually would go even further: Making
> attribute instances aware of their target feels like a layering
> violation. Attributes are intended to provide metadata, not behavior.
> The behavior can then be added by whoever is consuming the attribute.
>
> The RFC itself contains one example with two possible use cases:
>
> - Further narrowing down TARGET_CLASS targets. For that I feel the
> correct solution would be further splitting the target constants into
> TARGET_CLASS_ONLY, TARGET_INTERFACE, TARGET_TRAIT, etc.
>
> - Adding side-effects to a constructor, specifically side-effects that
> need to rely on global state. This is the layering violation I mentioned
> above: This kind of logic should be performed by the service that is
> reading out and constructing the attribute - something that necessarily
> exists -, not by the attribute itself.
>
> Best regards
> Tim Düsterhus
Here's another real-world example that I use i Serde, via AttributeUtils (slightly modified and
simplified to make it clearer):
#[Attribute(Attribute::TARGET_PROPERTY)]
class Field implements FromReflectionParameter {
public function __construct(
public private(set) ?string $name = null,
public private(set) ?string $type = null,
) {}
public function fromReflectionParameter(ReflectionProperty $rProp): void {
$this->name ??= $rProp->getName();
$this->type ??= $rProp->getType()->getName();
}
}
class Example {
#[Field]
public string $a;
#[Field(name: 'second');
public string $b;
}
In this case, the attribute needs, by definition, to know the name and type of the property
it's on, but that can be overridden. Any serializer or ORM is going to need to address this
use case in some form or another; I don't know off hand how Symfony Serializer or Doctrine
handle it, but in Serde I took the "setter injection" approach, triggered by the presence
of an interface.
This does work, and is in production now. But as Daniel and others have noted, it means
there's a gap period where the object could be in an invalid state, because construction is
split across multiple startup methods. (The real code has a whole lot more than just one additional
setter callbacks.) It also means that trying to construct the attribute object with reflection
yourself, rather than going through AttributeUtils' API, would lead to a broken object since
the secondary pseudo-constructors don't get called.
What this RFC would allow is rewriting the above as:
#[Attribute(Attribute::TARGET_PROPERTY)]
class Field {
public function __construct(
public private(set) ?string $name = null,
public private(set) ?string $type = null,
) {
if ($rProp = ReflectionAttribute::getCurrent()->getReflectionTarget()) {
$this->name ??= $rProp->getName();
$this->type ??= $rProp->getType()->getName();
}
}
}
Now the same functionality is available natively without going through AttributeUtils. In fact, in
concept most of AttributeUtils could get rewritten so that instead of a bunch of triggering
interfaces with multiple rather boilerplate methods, you could do something like this:
#[Attribute(Attribute::TARGET_CLASS)]
class SomeClass {
public readonly array $props;
public readonly array $consts;
public function __construct(
public private(set) ?string $name = null,
) {
if ($rClass = ReflectionAttribute::getCurrent()->getReflectionTarget()) {
$this->name ??= $rClass->getName();
new AttributeUtils\GetProperties($this, $rClass, Field::class, fn(array $ps) =>
$this->props = $ps)->load();
new AttributeUtils\GetConstants($this, $rClass, ConstAttribute::class, fn(array $cs)
=> $this->consts = $cs)->load();
// ...
}
}
}
I've been toying with a new API that looks more like that, but in a separate method. This
would move that logic fully inside the constructor, and eliminate a whole bunch of noisy methods and
interfaces.
It's not perfect, certainly. Constructing the attribute manually for testing purposes would
still pose a risk of incomplete data, unless you account for that in the constructor.
That is a very valid, relevant, and common use case, which this RFC would simplify. I don't
like the modal nature of it either, but so far no one has suggested a better alternative. (And no,
"just do it all externally and transfer it to some other non-attribute object" is not a
better alternative. It's a crapton more pointless work for no benefit that makes the code
harder to follow.)
--Larry Garfield