Skip to content

Commit e949de1

Browse files
committed
Allow class inheritance determination for enums
### Motivation In the scope of [a downstream project issue](Crell/Serde#10), It was noticed that in cases where an enum implemented an interface, the interface attributes could not be read. This change allows `ReflectionEnum` to retrieve such information if it exists
1 parent 8c52fcf commit e949de1

4 files changed

Lines changed: 98 additions & 2 deletions

File tree

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,64 @@ In this case, a class may be marked with either `Screen` or `Audio`, but not bot
818818

819819
In this example, `$displayInfoA->type` will be an instance of `Screen`, `$displayInfoB->type` will be an instance of `Audio`, and `$displayInfoC->type` will be `null`.
820820

821+
### Interface using attributes
822+
823+
Interfaces may also implement attributes. This is useful for cases in which you want to define a type map at the interface level:
824+
825+
Note: in this case, make sure the class implements the `Inheritable` interface, to return the expected values.
826+
827+
```php
828+
829+
use Crell\AttributeUtils\Inheritable;
830+
831+
#[Attribute(Attribute::TARGET_CLASS)]
832+
final readonly class TypeMap implements Inheritable
833+
{
834+
/**
835+
* @param non-empty-string $key
836+
* @param array<non-empty-string, class-string> $map
837+
*/
838+
public function __construct(
839+
public string $key,
840+
public array $map,
841+
) {
842+
}
843+
}
844+
845+
#[TypeMap(
846+
key: 'type',
847+
map: [
848+
'enum' => MyEnum::class,
849+
'object' => MyObject::class,
850+
]
851+
)]
852+
interface Something
853+
{
854+
}
855+
856+
enum MyEnum: string implements Something
857+
{
858+
case A = 'a';
859+
case B = 'b';
860+
}
861+
862+
class MyObject implements Something
863+
{
864+
public function __construct(
865+
public int $id,
866+
) {
867+
}
868+
}
869+
870+
$analyzer = new Crell\AttributeUtils\Analyzer();
871+
$attrib = $analyzer->analyze(MyEnum::class, TypeMap::class);
872+
873+
//// $attrib is now an instance of Map.
874+
print $attrib->key . PHP_EOL; // Prints 'type'
875+
print $attrib->map['enum'] . PHP_EOL; // Prints `MyEnum`
876+
print $attrib->map['object'] . PHP_EOL; // Prints `MyObject`
877+
```
878+
821879
## Change log
822880

823881
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

src/AttributeParser.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,7 @@ protected function attributeInheritanceTree(\Reflector $subject, string $attribu
224224
\ReflectionMethod::class => $this->classElementInheritanceTree($subject),
225225
\ReflectionClassConstant::class => $this->classElementInheritanceTree($subject),
226226
\ReflectionParameter::class => $this->parameterInheritanceTree($subject),
227-
// If it's an enum, there's nothing to inherit so just stub that out.
228-
\ReflectionEnum::class => [],
227+
\ReflectionEnum::class => $this->enumInheritanceTree($subject),
229228
};
230229
}
231230
}
@@ -248,6 +247,24 @@ protected function classInheritanceTree(\ReflectionClass $subject): iterable
248247
}
249248
}
250249

250+
/**
251+
* Returns all the ReflectionClasses in an enum subject's inheritance tree.
252+
*
253+
* This includes only interfaces, as enums cannot extend other classes.
254+
*
255+
* @param \ReflectionEnum<\UnitEnum> $subject
256+
* The reflection of the enum for which we want the ancestors.
257+
* @return iterable<\ReflectionClass<object>>
258+
* @throws \ReflectionException
259+
*/
260+
protected function enumInheritanceTree(\ReflectionEnum $subject): iterable
261+
{
262+
$ancestors = $this->classAncestors($subject->getName(), false);
263+
foreach ($ancestors as $ancestor) {
264+
yield new \ReflectionClass($ancestor);
265+
}
266+
}
267+
251268
/**
252269
* Returns all of the ReflectionParameters in a subject's inheritance tree.
253270
*

tests/ClassAnalyzerTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
use Crell\AttributeUtils\Records\ClassWithScopesMulti;
5050
use Crell\AttributeUtils\Records\ClassWithScopesNotDefault;
5151
use Crell\AttributeUtils\Records\ClassWithSubAttributes;
52+
use Crell\AttributeUtils\Records\EnumWithInterface;
5253
use Crell\AttributeUtils\Records\LabeledApp;
5354
use Crell\AttributeUtils\Records\MissingPropertyAttributeArguments;
5455
use Crell\AttributeUtils\Records\MultiuseClass;
@@ -416,6 +417,15 @@ public static function attributeTestProvider(): \Generator
416417
},
417418
];
418419

420+
yield 'Enum implementing interface' => [
421+
'subject' => EnumWithInterface::class,
422+
'attribute' => BasicClass::class,
423+
'test' => static function(mixed $classDef) {
424+
self::assertEquals(5, $classDef->a);
425+
self::assertEquals(10, $classDef->b);
426+
},
427+
];
428+
419429
yield 'Field takes defaults from class' => [
420430
'subject' => PropertyThatTakesClassDefault::class,
421431
'attribute' => PropertyTakesClassDefaultClass::class,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Crell\AttributeUtils\Records;
6+
7+
enum EnumWithInterface: int implements AnInterface
8+
{
9+
case A = 1;
10+
case B = 2;
11+
}

0 commit comments

Comments
 (0)