Chris Haas
Chris Haas

Reputation: 55427

How do you access attributes on enum cases?

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?

Upvotes: 2

Views: 1312

Answers (3)

Chris Haas
Chris Haas

Reputation: 55427

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.

Upvotes: 0

lukas.j
lukas.j

Reputation: 7163

#[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'

Upvotes: 4

lukas.j
lukas.j

Reputation: 7163

#[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)

Upvotes: 3

Related Questions