2

According to the RFC on Enumerations, attributes can be added to cases by using Attribute::TARGET_CLASS_CONSTANT. (Actually, the RFC says TARGET_CLASS_CONST but that is either a typo or a later change.) I'm having trouble trying to access them using Reflection, however.

Given this setup:

#[Attribute(Attribute::TARGET_CLASS_CONSTANT)]
class TestAttribute
{
    public function __construct(public string $value)
    {
    }
}

enum TestNum
{
    #[TestAttribute('alpha value')]
    case ALPHA;

    #[TestAttribute('beta value')]
    case BETA;
}

I would expect the following code to give me an array with a single attribute, however it returns an empty array.

$obj = TestNum::ALPHA;
$reflection = new ReflectionClass($obj);
$classAttributes = $reflection->getAttributes(TestAttribute::class);
var_dump($classAttributes);

Demo here: https://3v4l.org/uLDVQ#v8.1.2

I found a test-case for this in the PHP src, however the usage isn't what I'd expect. Instead of using an instance, I need to decompose it:

var_dump((new \ReflectionClassConstant(TestNum::class, 'ALPHA'))->getAttributes(TestAttribute::class)[0]->newInstance());

Demo here: https://3v4l.org/BsA9r#v8.1.2

I can use that format, but it feels really hacky since I'm pretty much using reflection inside of reflection:

var_dump((new \ReflectionClassConstant($obj::class, $obj->name))->getAttributes(TestAttribute::class)[0]->newInstance());

Demo here: https://3v4l.org/YY6Oa#v8.1.2

Specifically, the new \ReflectionClassConstant($obj::class, $obj->name) pattern seems strangely boilerplate.

Is there another way to access individual enumeration case attributes that I'm missing?

Chris Haas
  • 53,986
  • 12
  • 141
  • 274
  • 2
    Not sure if this fits what you're trying to do. At first glance, you can access attributes from `getCase()` `$attributes = (new ReflectionEnum(TestNum::class))->getCase("ALPHA")->getAttributes(TestAttribute::class);` – Clément Baconnier Jan 25 '22 at 22:23
  • Thanks @ClémentBaconnier. What I really want is to be able to access attributes on `$obj` when I do `$obj = TestNum::ALPHA;`. Your version still needs to manually specify the enum/class and the case, the latter as a string. I can use `$obj::class` and `$obj->name`, as noted above, but both `ReflectionEnum` and `ReflectionClassConstant` say that they can use an `$objectOrClass`, and I'm trying to use the "object" version. I don't know if this is related to being backed by a singleton, although I'm not sure why that would matter. – Chris Haas Jan 25 '22 at 22:37
  • I have difficulties to understand what you are trying to achieve. As far as I understand you want to get the value from TestAttribute ('alpha value') but only use TestNum::ALPHA and no string like 'ALPHA'. As far as I know this is not possible. I also do not know of any possible use case for this. – lukas.j Jan 25 '22 at 22:55
  • I posted a second answer (but I'm still curious to learn what your goal is). – lukas.j Jan 25 '22 at 23:06
  • @lukas.j, as to a possible use-case, the RFC explicitly allowed this for a reason, and the [discussion](https://externals.io/message/112626#112769) actually wanted to include `TARGET_CASE` or similar for a bit. I would expect that using an attribute on an enum case to effectively be the same as using an attribute on a subclass of an abstract class, so the use-case reasons would be the same. Reading through the discussion further, it really sounds like there was a thought to adding more Reflection things but they decided, for whatever reason, not to pursue it for the time being. – Chris Haas Jan 25 '22 at 23:22
  • What I'm trying to achieve is to avoid using `'ALPHA'` or `$obj->name`. That absolutely works. But writing that feels like writing `(new ReflectionClass(Something::class, $obj))` (not valid, just an example) where `$obj` is an instance of that class, so passing the class is redundant. I've identified three possible ways to get the values, my one noted (`ReflectionClassConstant`), your `getReflectionConstant` and `ReflectionEnum` from Clement, but none work on instances. So I think my answer is "no, instances are singletons bound to class constants and treated as such for reflection purposes." – Chris Haas Jan 25 '22 at 23:39
  • I still do not understand a possible use case. Your code in your OP has the line _$obj = TestNum::ALPHA;_. What is the problem with using _$obj->name_ later on? I think you are trying to achieve something which is non-sensical because at some point somewhere in your code you need to set $obj. And by doing that you automatically gain access to its class and name. So it would be great if you could state what you are trying to achieve. – lukas.j Jan 26 '22 at 09:53

3 Answers3

3
#[Attribute( Attribute::TARGET_CLASS_CONSTANT )]
class TestAttribute {
  public function __construct(public string $value) {
  }
}

enum TestNum {
  #[TestAttribute( 'alpha value' )]
  case ALPHA;

  #[TestAttribute( 'beta value' )]
  case BETA;
}


$obj = TestNum::ALPHA;
$ref = (new ReflectionClass($obj))->getReflectionConstant('ALPHA');

var_dump($ref->getAttributes()[0]->getArguments());   // alpha value
var_dump($ref->getAttributes()[0]->getName());        // TestAttribute
var_dump($ref->getName());                            // ALPHA
var_dump($ref->getValue());                           // enum(TestNum::ALPHA)
lukas.j
  • 6,453
  • 2
  • 5
  • 24
  • Thanks @lukas.j. As noted in my question, I know that I can explicitly name the enum using `TestNum::class` , and then explicitly reference the case using either a string literal or an instance's `name` field, but I'm wondering if there is a way to reference the instance itself, which in my case is `$obj`. – Chris Haas Jan 25 '22 at 22:34
  • 1
    I changed the code, but there is no difference between handing over TestNum::ALPHA or TestNum::class to new ReflectionClass(). – lukas.j Jan 25 '22 at 22:45
3
#[Attribute( Attribute::TARGET_CLASS_CONSTANT )]
class TestAttribute {
  public function __construct(public string $value) {
  }
}

enum TestNum {
  #[TestAttribute( 'alpha value' )]
  case ALPHA;

  #[TestAttribute( 'beta value' )]
  case BETA;
}

$obj = TestNum::ALPHA;
$ref = new ReflectionEnumUnitCase($obj::class, $obj->name);
$argument = $ref->getAttributes('TestAttribute')[0]->getArguments()[0];

print_r($argument);   // Prints: 'alpha value'
lukas.j
  • 6,453
  • 2
  • 5
  • 24
  • This one worked for me. [The other solution](https://stackoverflow.com/a/70856527) from did not work for me. – Uwe Keim Aug 27 '22 at 19:43
0

Thanks both to lukas.j and Clément Baconnier for their comments and answers.

I think my problem is ultimately my mental model of the enum. In my mind, an enum is syntactic sugar around abstract classes and final sub-classes:

abstract class TestNum
{
    // Not valid, just representative
    const ALPHA = new Alpha();
}
final class Alpha extends TestNum{};

But in reality, enums are closer to:

final class TestNum
{
    private $map;

    // Not valid, just representative
    const ALPHA = self::createInstance('ALPHA');

    private function createInstance($key) : self
    {
        $map[$key] = new self();
        return $map[$key];
    }
}

If you look at my (incorrect) mental model, the use-case for attributes on an enum case are the same reasons for attributes on classes.

So the answer to my question is no. The fact that you pass TestNum::ALPHA to reflection, either directly or through an instance, does not mean you are reflecting upon that instance, you are only ever reflecting on TestNum. This means there's zero difference between these three:

  • new ReflectionClass(TestNum::class)
  • new ReflectionClass(TestNum::ALPHA)
  • new ReflectionClass(TestNum::ALPHA::class)

Yes, you can get ALPHA by explicitly asking for it by a named-string, either as a literal, or through the name property on an enum. But reflection doesn't "know" that TestNum::ALPHA is the ALPHA instance of TestNum. To say that another way, there's no reflectable internal state to TestNum::ALPHA to differentiate it from TestNum::BETA.

Chris Haas
  • 53,986
  • 12
  • 141
  • 274