Skip to content
Closed
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
121 changes: 120 additions & 1 deletion packages/reflection/src/PropertyReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@
namespace Tempest\Reflection;

use Error;
use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use PhpParser\ParserFactory;
use ReflectionProperty as PHPReflectionProperty;

final class PropertyReflector implements Reflector
{
use HasAttributes;

/** @var array<string, array<string, string>> */
private static array $useStatementCache = [];

public function __construct(
private readonly PHPReflectionProperty $reflectionProperty,
) {}
Expand Down Expand Up @@ -104,7 +111,119 @@ public function getIterableType(): ?TypeReflector
return null;
}

return new TypeReflector(ltrim($match[1], '\\'));
$typeName = ltrim($match[1], '\\');

if (str_contains($match[1], '\\')) {
return new TypeReflector($typeName);
}

return new TypeReflector($this->resolveShortClassName($typeName));
}

private function resolveShortClassName(string $shortName): string
{
if (class_exists($shortName)) {
return $shortName;
}

$declaringClass = $this->reflectionProperty->getDeclaringClass();
$fileName = $declaringClass->getFileName();

if ($fileName && ($resolved = $this->resolveFromUseStatements($fileName, $shortName))) {
return $resolved;
}

$namespace = $declaringClass->getNamespaceName();

if ($namespace !== '') {
$fqcn = $namespace . '\\' . $shortName;

if (class_exists($fqcn)) {
return $fqcn;
}
}

return $shortName;
}

private function resolveFromUseStatements(string $fileName, string $shortName): ?string
{
$useStatements = $this->getUseStatements($fileName);

return $useStatements[$shortName] ?? null;
}

/** @return array<string, string> */
private function getUseStatements(string $fileName): array
{
return self::$useStatementCache[$fileName] ??= $this->parseUseStatements($fileName);
}

/** @return array<string, string> */
private function parseUseStatements(string $fileName): array
{
$content = file_get_contents($fileName);

if ($content === false) {
return [];
}

$ast = new ParserFactory()
->createForNewestSupportedVersion()
->parse($content);

if ($ast === null) {
return [];
}

$useStatements = [];
$traverser = new NodeTraverser();

$traverser->addVisitor(new class($useStatements) extends NodeVisitorAbstract {
public function __construct(
private array &$useStatements,
) {}

public function enterNode(Node $node): null
{
match (true) {
$node instanceof Node\Stmt\Use_ && $node->type === Node\Stmt\Use_::TYPE_NORMAL => $this->extractUseItems($node->uses),
$node instanceof Node\Stmt\GroupUse => $this->extractGroupUseItems($node),
default => null,
};

return null;
}

/** @param Node\UseItem[] $uses */
private function extractUseItems(array $uses, string $prefix = ''): void
{
foreach ($uses as $use) {
$fqcn = $prefix . $use->name->toString();
$alias = $use->alias->name ?? $use->name->getLast();
$this->useStatements[$alias] = $fqcn;
}
}

private function extractGroupUseItems(Node\Stmt\GroupUse $node): void
{
$prefix = $node->prefix->toString() . '\\';

foreach ($node->uses as $use) {
if ($use->type !== Node\Stmt\Use_::TYPE_NORMAL) {
continue;
}

$fqcn = $prefix . $use->name->toString();
$alias = $use->alias->name ?? $use->name->getLast();
$this->useStatements[$alias] = $fqcn;
}
}
});

$traverser->traverse($ast);

return $useStatements;
}

public function isUninitialized(object $object): bool
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Tempest\Reflection\Tests\Fixtures\IterableTypeResolution;

final class Author
{
public string $name;
}
10 changes: 10 additions & 0 deletions packages/reflection/tests/Fixtures/IterableTypeResolution/Book.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Tempest\Reflection\Tests\Fixtures\IterableTypeResolution;

final class Book
{
public string $title;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Tempest\Reflection\Tests\Fixtures\IterableTypeResolution\Models;

use Tempest\Reflection\Tests\Fixtures\IterableTypeResolution\Book as MyBook;

final class AliasedUseStatement
{
/** @var MyBook[] */
public array $books = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Tempest\Reflection\Tests\Fixtures\IterableTypeResolution\Models;

final class FullyQualifiedClassName
{
/** @var \Tempest\Reflection\Tests\Fixtures\IterableTypeResolution\Book[] */
public array $books = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Tempest\Reflection\Tests\Fixtures\IterableTypeResolution\Models;

use Tempest\Reflection\Tests\Fixtures\IterableTypeResolution\Author;
use Tempest\Reflection\Tests\Fixtures\IterableTypeResolution\Book;

final class GroupUseStatement
{
/** @var Book[] */
public array $books = [];

/** @var Author[] */
public array $authors = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Tempest\Reflection\Tests\Fixtures\IterableTypeResolution\Models;

use Tempest\Reflection\Tests\Fixtures\IterableTypeResolution\Book;

final class RegularUseStatement
{
/** @var Book[] */
public array $books = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Tempest\Reflection\Tests\Fixtures\IterableTypeResolution;

final class SameNamespace
{
/** @var Book[] */
public array $books = [];
}
79 changes: 79 additions & 0 deletions packages/reflection/tests/PropertyReflectorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace Tempest\Reflection\Tests;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Tempest\Reflection\PropertyReflector;
use Tempest\Reflection\Tests\Fixtures\IterableTypeResolution\Author;
use Tempest\Reflection\Tests\Fixtures\IterableTypeResolution\Book;
use Tempest\Reflection\Tests\Fixtures\IterableTypeResolution\Models\AliasedUseStatement;
use Tempest\Reflection\Tests\Fixtures\IterableTypeResolution\Models\FullyQualifiedClassName;
use Tempest\Reflection\Tests\Fixtures\IterableTypeResolution\Models\GroupUseStatement;
use Tempest\Reflection\Tests\Fixtures\IterableTypeResolution\Models\RegularUseStatement;
use Tempest\Reflection\Tests\Fixtures\IterableTypeResolution\SameNamespace;

final class PropertyReflectorTest extends TestCase
{
#[Test]
public function iterable_type_with_regular_use_statement(): void
{
$property = PropertyReflector::fromParts(RegularUseStatement::class, 'books');

$iterableType = $property->getIterableType();

$this->assertNotNull($iterableType);
$this->assertSame(Book::class, $iterableType->getName());
}

#[Test]
public function iterable_type_with_aliased_use_statement(): void
{
$property = PropertyReflector::fromParts(AliasedUseStatement::class, 'books');

$iterableType = $property->getIterableType();

$this->assertNotNull($iterableType);
$this->assertSame(Book::class, $iterableType->getName());
}

#[Test]
public function iterable_type_with_group_use_statement(): void
{
$booksProperty = PropertyReflector::fromParts(GroupUseStatement::class, 'books');
$authorsProperty = PropertyReflector::fromParts(GroupUseStatement::class, 'authors');

$booksType = $booksProperty->getIterableType();
$authorsType = $authorsProperty->getIterableType();

$this->assertNotNull($booksType);
$this->assertSame(Book::class, $booksType->getName());

$this->assertNotNull($authorsType);
$this->assertSame(Author::class, $authorsType->getName());
}

#[Test]
public function iterable_type_with_same_namespace(): void
{
$property = PropertyReflector::fromParts(SameNamespace::class, 'books');

$iterableType = $property->getIterableType();

$this->assertNotNull($iterableType);
$this->assertSame(Book::class, $iterableType->getName());
}

#[Test]
public function iterable_type_with_fully_qualified_class_name(): void
{
$property = PropertyReflector::fromParts(FullyQualifiedClassName::class, 'books');

$iterableType = $property->getIterableType();

$this->assertNotNull($iterableType);
$this->assertSame(Book::class, $iterableType->getName());
}
}
Loading