On Sat, May 16, 2026, at 8:30 PM, Benjamin Außenhofer wrote:

>> 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.
>
> But you have Serde or AttributeUtils making the instances of Field or 
> not? I mean you already have the code in userland already that makes 
> this working, why does it need to be in core with this specific API 
> that is not obvious to readers and not idiomatic PHP. 
>> 
>> 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.
>
> I don’t think this is a powerful enough argument, there are many cases 
> where there are gaps where objects are not in a valid state yet.

I have made that same point myself in the past many times; yet all the SA tools 
on the market right now still yell at me for writing to a readonly property 
from anywhere other than the constructor.

>> 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();
>>        }
>>    }
>> }
>
> The problem with this code is that it needs explicit if and support to 
> make it testable at all by still allowing to pass name and type in the 
> constructor. Which proves Tim’s argument that this is too tightly 
> coupled.
>
> With the way ReflectionAttribute defers the constuction of the 
> attribute, you can close the gap yourself.
>
> if (is_a($reflectionAttribute->getName(), FromReflectionParameter::class) {
>      $className = $reflectionAttribute->getName();
>      $attribute = $className::fromReflectionParameter($reflector);
> } else {
>     $attribute = $reflectionAttribute->newInstance();
> }

I don't see what purpose that serves...

What AttributeUtils does now is approximately this (again, simplified for 
clarity):

public function analyze(string $class, string $attribute) {
  $rClass = new ReflectionClass($class);
  $classDef = $rClass->getAttributes($attribute, 
ReflectionAttribute::IS_INSTANCEOF)[0] ?? new $attribute();

  if ($classDef instanceof FromReflectionClass) {
    $classDef->fromReflection($rClass);
  }

  if ($classDef instanceof ParseProperties) {
    // Use reflection to get attributes off the properties of the class.
    $classDef->setProperties($properties);
  }
  // And similar code for other components, hard coded.
}

Again, this does work, but it means you *must* go through AU's analyze() method 
or nothing works.

I fully agree with the concerns that this is too modal/global.  But so far, 
this seems the least bad way to address the issue of attributes being 
fundamentally dumb about their context.  If someone has a better approach for 
handling that, please please do share it.

I will also note the bonus feature in this RFC, the ReflectionAttributeTarget 
interface.  I frankly want that even more than the reflection target. :-)  Even 
if this RFC fails, that should be brought back in its own little RFC and needs 
to pass.

--Larry Garfield

Reply via email to