Re: [RFC idea] Target-aware attributes
| From: | Rob Landers | Date: | Fri, 04 Jul 2025 22:55:05 +0000 |
| Subject: | Re: [RFC idea] Target-aware attributes | ||
| References: | 1 2 3 4 5 6 7 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-127893@lists.php.net to get a copy of this message | ||
On Sat, Jul 5, 2025, at 00:47, Andreas Hennings wrote:
> On Sat, 5 Jul 2025 at 00:11, Rob Landers <rob@bottled.codes> wrote:
> >
> >
> >
> > On Fri, Jul 4, 2025, at 17:21, Andreas Hennings wrote:
> >
> > On Fri, 4 Jul 2025 at 06:30, Stephen Reay <php-lists@koalephant.com> wrote:
> > >
> > >
> > >
> > > On 4 Jul 2025, at 00:54, Andreas Hennings <andreas@dqxtech.net> wrote:
> > >
> > > On Thu, 3 Jul 2025 at 19:17, Stephen Reay <php-lists@koalephant.com> wrote:
> > >
> > >
> > >
> > >
> > >
> > > Sent from my iPhone
> > >
> > > On 3 Jul 2025, at 23:40, Larry Garfield <larry@garfieldtech.com> wrote:
> > >
> > > On Wed, Jul 2, 2025, at 5:26 PM, Andreas Hennings wrote:
> > >
> > > This topic was discussed in the past as "Declaration-aware
> > > attributes", and mentioned in the discussion to "Amendments to
> > > Attributes".
> > > I now want to propose a close-to-RFC iteration of this.
> > > (I don't have RFC Karma, my wiki account is "Andreas Hennings
> > > (donquixote)")
> > >
> > > -----
> > >
> > > Primary proposal
> > > =============
> > >
> > > I propose to introduce 3 new methods on ReflectionAttribute.
> > >
> > > static ReflectionAttribute::getCurrentTargetReflector(): ?Reflector
> > > Most of the time, this will return NULL.
> > > During the execution of ReflectionAttribute->newInstance(), it will
> > > return the reflector of the symbol on which the attribute is found.
> > > (in other words, during
> > > $reflector->getAttributes()[$i]->newInstance(), it will return
> > > $reflector.)
> > > During the execution of
> > > ReflectionAttribute::invokeWithTargetAttribute($target, $callback), it
> > > will return $target.
> > > If the call stack contains multiple calls to the above mentioned
> > > methods, only the closest/deepest one counts.
> > > (This means that php needs to maintain a stack of reflectors.)
> > >
> > >
> > > *snip*
> > >
> > > Other alternatives
> > > ======================
> > >
> > > In older discussions, it was suggested to provide the target reflector
> > > as a special constructor parameter.
> > > This is problematic because an attribute expression #[MyAttribute('a',
> > > 'b', 'c')] expects to pass values to all the parameters.
> > >
> > > Another idea was to provide the target reflector through a kind of
> > > setter method on the attribute class.
> > > This can work, but it makes attribute classes harder to write, because
> > > the constructor does not have all the information.
> > > It may also prevent attribute classes from being stateless (depending
> > > how we define stateless).
> > >
> > >
> > > Userland implementations
> > > =========================
> > >
> > > One userland implementation that was mentioned in this list in the
> > > past is in the 'crell/attributeutils' package.
> > > This one uses a kind of setter injection for the target reflector.
> > > See
> > >
> > > https://github.com/Crell/AttributeUtils/blob/master/src/FromReflectionClass.php
> > >
> > >
> > > Hey, I know that guy! :-)
> > >
> > > Another userland implementation is in the
> > > 'ock/reflector-aware-attributes' package.
> > > https://github.com/ock-php/reflector-aware-attributes
> > > (I created that one)
> > > This supports both a setter method and getting the target reflector
> > > from the attribute constructor.
> > >
> > > The problem with any userland implementation is that it only works if
> > > the attribute is instantiated (or processed) using that userland
> > > library.
> > > Simply calling $reflector->getAttributes()[0]->newInstance() would
> > > either return an instance that is incomplete, or it would break, if
> > > the attribute class expects access to its target.
> > >
> > >
> > > I am unsurprisingly in favor of finding a solution here, as there are innumerable
> > > cases where you need the reflectable that the attribute is on; the most common for me is using the
> > > name/type of a property as defaults for the attribute.
> > >
> > > However, I am very skeptical about a stateful global value as the solution.
> > > We've tried very hard to remove those from PHP, mostly successfully. Adding another one back
> > > in feels like a major step backwards, and a great place for weird bugs to hide.
> > >
> > > A setter method injection is what I did in AttributeUtils, because it was the only
> > > real option. Alternatively, I suppose core could use property setter injection (either a magically
> > > named property like $__reflector, or a property that itself has an attribute on it, etc.). That
> > > would allow it to be set before the constructor is called, and with property hooks would allow
> > > processing either immediately or later in the constructor. The downside here is that Attribute are,
> > > generally, serializable, but a Reflection object is not. So if someone wanted a serializable
> > > attribute they would have to accept the property, use it, and then remember to unset it at some
> > > point. That's clumsy.
> > >
> > > --Larry Garfield
> > >
> > >
> > > As someone that's written yet another userland "solution" for this
> > > problem, I have an alternative solution, based somewhat on an internalised concept of "never
> > > store Reflectors".
> > >
> > > Introduce an interface "ReflectorAttribute" (bike shedding to come); which
> > > accepts a single Reflector argument.
> > >
> > > If the attribute implements the interface, the method is called immediately following
> > > instantiation.
> > >
> > >
> > > Yep, this is the "method injection" mentioned by Larry, or what I
> > > referred to as "setter injection".
> > > I have not seen your library, but I assume that's where it is going.
> > >
> > >
> > > Hi Andreas,
> > >
> > > I guess the key difference I wanted to highlight is that the existing discussion
> > > keeps referencing it as a "setter" - which I think from user land at least will generally
> > > be understood to mean setting a property on an object - which then adapted into Larry's mention
> > > of specifically setting a property before the constructor is run.
> >
> > I guess the main reason it "keeps" doing that is that an email is not
> > a wiki page that could be updated :)
> > I am happy to use different terminology in an RFC.
> > (with the current plan, it would only be mentioned under "alternatives
> > that were considered")
> > "method injection" seems fine.
> >
> > But then how would we name such a method?
> > In Larry's library it is ->fromReflection(..), but this is something I
> > would typically use for static factories.
> > To me, ->setReflector() is still ok even if internally it is not a
> > setter. The method describes the contract, not what happens inside.
> > Another idea would be ->tellAboutReflector() or maybe ->injectTargetReflector()?
> >
> > >
> > > I think it's a bad idea to reference this concept as "setting a
> > > property" - my understanding is that it's never a good idea to hang onto Reflector objects
> > > longer than absolutely necessary, so I don't think this feature should then result in people
> > > doing that due to the impression it gives (i.e. if it was referred to as
> > > "setReflection<subtype>()" or if the description for it is "allows an Attribute
> > > to store the Reflector target it was declared on" etc)
> > >
> >
> >
> > --------
> >
> > Now, to resolve the controversial part of this discussion.
> >
> > We could reduce the RFC to the uncontroversial part:
> > Provide a ReflectionAttribute->getTargetReflector().
> >
> > With this, the rest of the proposal can be implemented in userland
> > using debug_backtrace().
> > https://3v4l.org/Ilrqm#vnull
> >
> > #[Attribute]
> > class MyAttribute {
> > public function __construct() {
> > $target_reflector =
> > debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS |
> > DEBUG_BACKTRACE_PROVIDE_OBJECT, 3)[1]['object'];
> > }
> > }
> >
> > Yes it feels dirty, but now it is now longer something we have to
> > argue about in this list :)
> >
> >
> > Why do we have to call a constructor before we initialize a property?
> > https://3v4l.org/srdM6
> >
> > You can do this in regular php, but it is even simpler in the engine.
> >
> > — Rob
>
> This is interesting, but I still add some logistical clutter to the
> attribute class.
> It does bring some advantage over post-construction method injection,
> because you can do the logistics in a trait, and then in the
> constructor you have all the information available.
> But, even if that part is hidden away in the engine, now we have a
> property that we probably want to unset later.
>
> -- Andreas
>
I haven’t read the entire thread yet, but why would you want to unset it? In my experience of
working with attributes, they have a very short lifetime (usually to the end of the loop/function,
and no more) since they’re just metadata. The biggest boon I see here is the ability to easily
encapsulate some logic in my attributes without relying on 3rd party behavior. If some library
decides to keep them around, I will assume they know what they’re doing.
— Rob