Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions src/Reflection/ReflectionClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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;
}
}
Expand Down
30 changes: 30 additions & 0 deletions test/Reflection/ReflectionClassTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
24 changes: 24 additions & 0 deletions test/Reflection/fixtures/discriminator_map_class_const.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
namespace Test;

use Doctrine\ORM\Mapping as ORM;
use Hostnet\Component\AccessorGenerator\Attribute as AG;

/**
* Entity with a DiscriminatorMap that uses ::class syntax in attribute arguments.
* The T_CLASS tokens from ::class must not confuse getName() into thinking it found
* the class declaration before the actual 'abstract class' keyword.
*/
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorMap([
'child_a' => ChildA::class,
'child_b' => ChildB::class,
])]
abstract class ParentEntity
{
/**
* @AG\Generate(set="none")
*/
#[ORM\Column(type: 'string')]
private string $name;
}
Loading