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