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