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`.

I usually try to avoid this, this is why I proposed the constructor parameter.
This way, the object is never in an incomplete state.

(Side note: Personally I use the naming convention from*() for static
factory methods. I might use set*() for this one, but then again, it
is only a setter from the outside.)

>
> (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.

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);

>
> My preference would be for something along those lines to be implemented in 
> core.
>
> Importantly, we *MUST NOT* design it such that the reflection object gets 
> assigned to a property of the attribute.  Reflection objects are not 
> serializable.  Attributes will frequently be cached.  That means it forces 
> the attribute author to make the property nullable AND then unset it sometime 
> before the attribute gets serialized, or it will break.  That's a no-go.

There could be ways around this problem, but I agree we should avoid
it on design level.

>
> 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')]

>
> 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 can see an argument either direction here.
>
> --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

Reply via email to