Re: Declaration-aware attributes

From: Date: Tue, 30 May 2023 21:46:04 +0000
Subject: Re: Declaration-aware attributes
References: 1 2 3 4 5 6 7 8  Groups: php.internals 
Request: Send a blank email to internals+get-120478@lists.php.net to get a copy of this message
On Tue, 30 May 2023 at 22:45, Larry Garfield <larry@garfieldtech.com> wrote: > > On Tue, May 30, 2023, at 7:34 PM, Andreas Hennings wrote: > > On Tue, 30 May 2023 at 19:12, Larry Garfield <larry@garfieldtech.com> wrote: > >> > >> I've run into this issue in my attribute library as well > >> (https://github.com/Crell/AttributeUtils). What I do there is allow attributes to opt-in to a > >> callback method via interface. For example: > >> > >> #[\Attribute] > >> class AttribWithName implements FromReflectionClass > >> { > >> public readonly string $name; > >> > >> public function __construct(?string $name = null) > >> { > >> if ($name) { > >> $this->name = $name; > >> } > >> } > >> > >> public function fromReflection(\ReflectionClass $subject): void > >> { > >> $this->name ??= $subject->getShortName(); > >> } > >> } > > > > So it is technically from outside it is a setter, whereas from inside > > it is not really. > > Technically, this means you need a version of the attribute object > > that can exist without the values from the reflector. > > The constructor needs to initialize some "stub" values, until the > > setter is called. > > Every other method also needs to support the case when the setter has > > not been called yet, or possibly throw an exception. > > Also, your property type has to allow null, so ?string, > > not string. > > Except the property type is not null above? The argument is, but not the property. (I could > use CPP here with a null value instead, if we had asymmetric visibility.) True! My conception of readonly was distorted by the same static analysis tools that you complained about earlier! https://3v4l.org/CUgYv I think a more accurate label would be "write once". > > Also, if the interface is called by the reflection system itself as part of getInstance() then > yes, we can guarantee that it's been run, just like we can guarantee the constructor has run. The concern was that you can also construct the attribute object with a regular new AttribWithName() expression, and then you can omit the ->fromReflection() call. But we could say it is part of the contract that you have to call the method before the object is fully operational. > > >> (Side note: This is why static analysis tools that forbid writing to readonly > >> properties except from the constructor are wrong; it's also an example of where asymmetric > >> visibility would be superior to readonly. But I digress.) > > > > Technically there is no guarantee that the setters will be called > > before any other method, and only once. > > If these methods can write on readonly properties, then any method can. > > Unless we somehow mark these methods as special. > > There's nothing special about readonly properties there. An uninitialized non-readonly > property is no more or less susceptible to still being uninitialized when you need it. Singling out > readonly here is just dumb, and exacerbates the problems of readonly. > > > On the other hand, a wither method with "clone with" should be allowed > > to work on readonly properties. > > You could rewrite your method like this, once we have clone with: > > (or use the old-school syntax but without readonly) > > > > public function withReflector(\ReflectionClass $subject): static > > { > > return ($this->name !== NULL) > > ? $this > > : clone $this with (name: $subject->getShortName(); > > } > > > > Then in the discovery code: > > > > $attribute = $reflectionClass->getAttributes()[0]->newInstance(); > > $attribute = $attribute->withReflector($reflectionClass); > > In that library I actually have several opt-in post-constructor setup routines. The > documentation covers them all. Making them all withers would be just needlessly slow. Basically > it's an example of a "mutable and then locked" object, which I emulate by virtue of > only calling the setup methods from the library, and using readonly properties. > > >> That's why I think an opt-in interface is the way to go. It also avoids any > >> confusion around the constructor parameters, which is, based on this thread, a confusing area. :-) > > > > What do you think about a placeholder syntax to avoid confusion with a > > skipped parameter? > > Like > > > > #[A('x', ?', 'y')] > > Oh god no. For one, function calls are already complicated enough and the next thing we should > add there is some kind of partial application, which already discussed using ?. Second, that the > attribute cares about what it is attached to is not something the caller should care about. If the > user of the attribute needs to specify "and put the reflection object here" in the > attribute usage, we've failed. I'd vote against any such proposal in any form, whatever > the syntax. I was actually thinking of the partial application with the ? symbol. The metaphor would be that we create a partial callable from the `new A(.., ?)` expression, and then PHP calls that callable with the reflector argument. It would all just be a metaphor to justify the ? placeholder, because PHP would actually just skip the extra step and evaluate the expression in one go. But the only reason was to have something to avoid a skipped parameter. We could also put other placeholders that feel more like expressions, but I am not happy with that either. In the end I could also live with a method call as you propose. > > >> My second preference would be the ReflectionAttribute::inProgress() call in the > >> constructor, or something like that. I like that less because it's a stateful call, but it > >> would also reduce the issue with readonly properties (as in the example above) by making both > >> alternatives available in the constructor, so maybe it's an acceptable tradeoff. > > > > I would like to avoid anything that is stateful or that leaves an > > incomplete stub object. > > (But I think I made this clear enough, so..) > > I don't think it's going to be possible to have both. We either have a static call > of some kind (the inProgress() method above or similar) that only works sometimes (ie, while an > attribute is being constructed), or we have multiple setup methods (which the engine can ensure are > called, but technically that does mean the object has a moment where it's potentially > incomplete). I doubt we can have our cake and eat it too, here. > > Personally I think the "thou shalt not ever have an object that isn't perfectly > setup" rule is over-blown. It's a good guideline, but it has edge cases where it just > simply doesn't work. This is one of them. > > --Larry Garfield > > -- > PHP Internals - PHP Runtime Development Mailing List > To unsubscribe, visit: https://www.php.net/unsub.php >

« previous php.internals (#120478) next »