On Tue, 30 May 2023 at 22:45, Larry Garfield <la...@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 <la...@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 > -- PHP Internals - PHP Runtime Development Mailing List To unsubscribe, visit: https://www.php.net/unsub.php