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

Reply via email to