diff --git a/src/Reflection/ReflectionClass.php b/src/Reflection/ReflectionClass.php index 197beda..be9ba43 100644 --- a/src/Reflection/ReflectionClass.php +++ b/src/Reflection/ReflectionClass.php @@ -115,6 +115,12 @@ public function getFilename(): string * This is the simple class name and not the fully * qualified class name. * + * PHP 8 emits T_CLASS for both `class Foo` declarations and `Foo::class` expressions. + * When a class-level attribute like #[ORM\DiscriminatorMap(['a' => Child::class])] + * appears above the class keyword, those ::class tokens appear first in the stream. + * We skip any T_CLASS not immediately followed by T_STRING, since only a real + * declaration has the class name as its next token. + * * @throws Exception\ClassDefinitionNotFoundException * @throws \OutOfBoundsException */ @@ -130,18 +136,22 @@ public function getName(): string throw new ClassDefinitionNotFoundException('No class is found inside ' . $this->filename . '.', 0, $e); } - // Get the following token - if ($loc !== null) { - $loc = $tokens->next($loc); + // Loop past ::class expressions until we find a T_CLASS/T_TRAIT immediately + // followed by T_STRING, which is the real class or trait declaration. + while ($loc !== null) { + $next = $tokens->next($loc); + + if ($next !== null && $tokens->type($next) === T_STRING) { + $this->name = $tokens->value($next); + $this->class_location = $next; + break; + } + + // T_CLASS from a ::class expression — keep scanning. + $loc = $tokens->scan($loc, [T_CLASS, T_TRAIT]); } - // Make sure it is not :: but a name - if ($loc !== null && $tokens->type($loc) === T_STRING) { - // Read the name from the token - $this->name = $tokens->value($loc); - $this->class_location = $loc; - } else { - // Mark the name as NOT found (in contrast to not initialized) + if (!$this->name) { $this->name = false; } } diff --git a/test/Reflection/ReflectionClassTest.php b/test/Reflection/ReflectionClassTest.php index da9df1a..1b3ca58 100644 --- a/test/Reflection/ReflectionClassTest.php +++ b/test/Reflection/ReflectionClassTest.php @@ -255,6 +255,36 @@ public function testEmptyFileClassNotFoundException(): void $class->getName(); } + /** + * PHP 8 uses T_CLASS for both `class Foo` declarations and `Foo::class` constant expressions. + * When a class-level attribute such as #[ORM\DiscriminatorMap(['a' => Child::class])] appears + * above the class keyword, those ::class tokens come first in the token stream. Previously, + * getName() took the very first T_CLASS it found, saw that the next token was not T_STRING + * (it was `,` or `]`), and set $name = false — causing ClassDefinitionNotFoundException to + * be silently swallowed in getMetadataForClass(), resulting in zero properties being found + * and no MethodsTrait being generated. + * + * @throws ClassDefinitionNotFoundException + * @throws \Hostnet\Component\AccessorGenerator\Reflection\Exception\FileException + * @throws \OutOfBoundsException + */ + public function testGetNameSkipsClassConstantExpressionsInAttributeArguments(): void + { + $class = new ReflectionClass(__DIR__ . '/fixtures/discriminator_map_class_const.php'); + + // Before the fix this threw ClassDefinitionNotFoundException because the ::class + // tokens inside #[ORM\DiscriminatorMap([...::class, ...])] were mistaken for the + // class declaration. + self::assertEquals('ParentEntity', $class->getName()); + self::assertEquals('Test', $class->getNamespace()); + + // The properties must also be reachable; previously getMetadataForClass() caught + // the exception and returned an empty property list, so no MethodsTrait was written. + $properties = $class->getProperties(); + self::assertCount(1, $properties); + self::assertEquals('name', $properties[0]->getName()); + } + public function testBroken(): void { $class = new ReflectionClass(__DIR__ . '/fixtures/broken.php'); diff --git a/test/Reflection/fixtures/discriminator_map_class_const.php b/test/Reflection/fixtures/discriminator_map_class_const.php new file mode 100644 index 0000000..22e9bae --- /dev/null +++ b/test/Reflection/fixtures/discriminator_map_class_const.php @@ -0,0 +1,24 @@ + ChildA::class, + 'child_b' => ChildB::class, +])] +abstract class ParentEntity +{ + /** + * @AG\Generate(set="none") + */ + #[ORM\Column(type: 'string')] + private string $name; +}