diff --git a/src/Reflection/ReflectionClass.php b/src/Reflection/ReflectionClass.php index 792a3ab86..2a8688600 100644 --- a/src/Reflection/ReflectionClass.php +++ b/src/Reflection/ReflectionClass.php @@ -34,6 +34,7 @@ use Roave\BetterReflection\SourceLocator\Located\LocatedSource; use Roave\BetterReflection\Util\CalculateReflectionColumn; use Roave\BetterReflection\Util\GetLastDocComment; +use Stringable; use Traversable; use function array_combine; @@ -950,6 +951,27 @@ public function getTraits(): array ); } + /** + * @param array $interfaces + * + * @return array + */ + private function addStringableInterface(array $interfaces): array + { + if (array_key_exists(Stringable::class, $interfaces)) { + return $interfaces; + } + + foreach ($this->node->getMethods() as $methodNode) { + if ($methodNode->name->toLowerString() === '__tostring') { + $interfaces[Stringable::class] = $this->reflectClassForNamedNode(new Node\Name(Stringable::class)); + break; + } + } + + return $interfaces; + } + /** * Given an AST Node\Name, create a new ReflectionClass for the element. */ @@ -1276,14 +1298,14 @@ public function isIterateable(): bool } /** - * @return array + * @return array */ private function getCurrentClassImplementedInterfacesIndexedByName(): array { $node = $this->node; if ($node instanceof ClassNode) { - return array_merge( + $interfaces = array_merge( [], ...array_map( fn (Node\Name $interfaceName): array => $this @@ -1292,6 +1314,8 @@ private function getCurrentClassImplementedInterfacesIndexedByName(): array $node->implements, ), ); + + return $this->addStringableInterface($interfaces); } // assumption: first key is the current interface @@ -1315,7 +1339,7 @@ private function getInheritanceClassHierarchy(): array /** * This method allows us to retrieve all interfaces parent of the this interface. Do not use on class nodes! * - * @return array parent interfaces of this interface + * @return array parent interfaces of this interface * * @throws NotAnInterfaceReflection */ @@ -1328,7 +1352,8 @@ private function getInterfacesHierarchy(): array $node = $this->node; assert($node instanceof InterfaceNode); - return array_merge( + /** @var array $interfaces */ + $interfaces = array_merge( [$this->getName() => $this], ...array_map( fn (Node\Name $interfaceName): array => $this @@ -1337,6 +1362,8 @@ private function getInterfacesHierarchy(): array $node->extends, ), ); + + return $this->addStringableInterface($interfaces); } /** diff --git a/test/unit/Reflection/ReflectionClassTest.php b/test/unit/Reflection/ReflectionClassTest.php index fa37a69cd..56ea9a930 100644 --- a/test/unit/Reflection/ReflectionClassTest.php +++ b/test/unit/Reflection/ReflectionClassTest.php @@ -26,6 +26,7 @@ use Roave\BetterReflection\Reflector\ClassReflector; use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound; use Roave\BetterReflection\SourceLocator\Ast\Locator; +use Roave\BetterReflection\SourceLocator\Type\AggregateSourceLocator; use Roave\BetterReflection\SourceLocator\Type\ComposerSourceLocator; use Roave\BetterReflection\SourceLocator\Type\SingleFileSourceLocator; use Roave\BetterReflection\SourceLocator\Type\StringSourceLocator; @@ -2004,4 +2005,74 @@ class Foo $privateMethod = $reflection->getMethod('privateMethodRenamed'); self::assertTrue($privateMethod->isProtected()); } + + public function testHasStringableInterface(): void + { + $php = <<<'PHP' + astLocator), + BetterReflectionSingleton::instance()->sourceLocator(), + ])); + + $classImplementingStringable = $reflector->reflect('ClassHasStringable'); + self::assertContains('Stringable', $classImplementingStringable->getInterfaceNames()); + + $classNotImplementingStringable = $reflector->reflect('ClassHasStringableAutomatically'); + self::assertContains('Stringable', $classNotImplementingStringable->getInterfaceNames()); + + $interfaceExtendingStringable = $reflector->reflect('InterfaceHasStringable'); + self::assertContains('Stringable', $interfaceExtendingStringable->getInterfaceNames()); + + $interfaceNotExtendingStringable = $reflector->reflect('InterfaceHasStringableAutomatically'); + self::assertContains('Stringable', $interfaceNotExtendingStringable->getInterfaceNames()); + } + + public function testHasAllInterfacesWithStringable(): void + { + $php = <<<'PHP' + astLocator), + BetterReflectionSingleton::instance()->sourceLocator(), + ])); + + $class = $reflector->reflect('HasStringable'); + + self::assertSame(['Iterator', 'Traversable', 'Stringable'], $class->getInterfaceNames()); + } }