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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ You can find and compare releases at the [GitHub release page](https://github.co

## Unreleased

### Changed

- Discover directive classes through Composer's autoload maps instead of `haydenpierce/class-finder`, making `lighthouse:ide-helper` faster https://github.com/nuwave/lighthouse/pull/2768

### Removed

- Dependency on `haydenpierce/class-finder` https://github.com/nuwave/lighthouse/pull/2768

## v6.66.0

### Added
Expand Down
1 change: 0 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
"require": {
"php": "^8",
"ext-json": "*",
"haydenpierce/class-finder": "^0.4 || ^0.5",
"illuminate/auth": "^9 || ^10 || ^11 || ^12 || ^13",
"illuminate/bus": "^9 || ^10 || ^11 || ^12 || ^13",
"illuminate/contracts": "^9 || ^10 || ^11 || ^12 || ^13",
Expand Down
4 changes: 2 additions & 2 deletions src/Console/IdeHelperCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Introspection;
use GraphQL\Utils\SchemaPrinter;
use HaydenPierce\ClassFinder\ClassFinder;
use Illuminate\Console\Command;
use Nuwave\Lighthouse\Schema\AST\ASTCache;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\DirectiveLocator;
use Nuwave\Lighthouse\Schema\SchemaBuilder;
use Nuwave\Lighthouse\Schema\Source\SchemaSourceProvider;
use Nuwave\Lighthouse\Support\ComposerClassFinder;
use Nuwave\Lighthouse\Support\Contracts\Directive as DirectiveInterface;
use Symfony\Component\Console\Input\InputOption;

Expand Down Expand Up @@ -107,7 +107,7 @@ protected function scanForDirectives(array $directiveNamespaces): array

foreach ($directiveNamespaces as $directiveNamespace) {
/** @var array<class-string> $classesInNamespace */
$classesInNamespace = ClassFinder::getClassesInNamespace($directiveNamespace);
$classesInNamespace = ComposerClassFinder::directClassesInNamespace($directiveNamespace);

foreach ($classesInNamespace as $class) {
$reflection = new \ReflectionClass($class);
Expand Down
4 changes: 2 additions & 2 deletions src/Schema/DirectiveLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\Node;
use HaydenPierce\ClassFinder\ClassFinder;
use Illuminate\Container\Container;
use Illuminate\Contracts\Events\Dispatcher as EventsDispatcher;
use Illuminate\Support\Collection;
Expand All @@ -13,6 +12,7 @@
use Nuwave\Lighthouse\Exceptions\DirectiveException;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Support\ComposerClassFinder;
use Nuwave\Lighthouse\Support\Contracts\Directive;
use Nuwave\Lighthouse\Support\Utils;

Expand Down Expand Up @@ -81,7 +81,7 @@ public function classes(): array

foreach ($this->namespaces() as $directiveNamespace) {
/** @var array<class-string> $classesInNamespace */
$classesInNamespace = ClassFinder::getClassesInNamespace($directiveNamespace);
$classesInNamespace = ComposerClassFinder::directClassesInNamespace($directiveNamespace);

foreach ($classesInNamespace as $class) {
$reflection = new \ReflectionClass($class);
Expand Down
80 changes: 80 additions & 0 deletions src/Support/ComposerClassFinder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php declare(strict_types=1);

namespace Nuwave\Lighthouse\Support;

use Composer\Autoload\ClassLoader;

class ComposerClassFinder
{
/**
* Find class-strings declared directly in the given namespace using Composer.
*
* @return list<class-string>
*/
public static function directClassesInNamespace(string $namespace): array
{
$namespace = rtrim($namespace, '\\') . '\\';
$loader = self::composerClassLoader();

Comment on lines +14 to +18
/** @var array<class-string, true> $classes */
$classes = [];

// Authoritative classmap (populated by `composer dump-autoload --optimize`).
foreach ($loader->getClassMap() as $fqcn => $_) {
if (! str_starts_with($fqcn, $namespace)) {
continue;
}

if (str_contains(substr($fqcn, strlen($namespace)), '\\')) {
continue;
}

if (class_exists($fqcn)) {
$classes[$fqcn] = true;
}
}

// PSR-4 prefix map: scan the matching directory's direct *.php children.
foreach ($loader->getPrefixesPsr4() as $prefix => $directories) {
if (! str_starts_with($namespace, $prefix)) {
continue;
}

$sub = str_replace('\\', DIRECTORY_SEPARATOR, substr($namespace, strlen($prefix)));
foreach ($directories as $directory) {
$target = rtrim($directory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $sub;
if (! is_dir($target)) {
continue;
}

foreach (\Safe\scandir($target) as $entry) {
if (! str_ends_with($entry, '.php')) {
continue;
}

/** @var class-string $fqcn */
$fqcn = $namespace . substr($entry, 0, -4);
if (class_exists($fqcn)) {
$classes[$fqcn] = true;
}
}
}
}

/** @var list<class-string> $result */
$result = array_keys($classes);

return $result;
}

protected static function composerClassLoader(): ClassLoader
{
foreach (spl_autoload_functions() ?: [] as $autoloader) {
if (is_array($autoloader) && $autoloader[0] instanceof ClassLoader) {
return $autoloader[0];
}
}

throw new \RuntimeException('Composer ClassLoader was not found among registered autoloaders.');
}
}
84 changes: 84 additions & 0 deletions tests/Unit/Support/ComposerClassFinderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php declare(strict_types=1);

namespace Tests\Unit\Support;

use Nuwave\Lighthouse\Cache\CacheDirective;
use Nuwave\Lighthouse\Cache\CacheKeyAndTags;
use Nuwave\Lighthouse\Cache\CacheServiceProvider;
use Nuwave\Lighthouse\Support\AppVersion;
use Nuwave\Lighthouse\Support\ComposerClassFinder;
use Nuwave\Lighthouse\Support\DriverManager;
use Nuwave\Lighthouse\Support\Utils;
use Tests\Console\UnionDirective;
use Tests\TestCase;

final class ComposerClassFinderTest extends TestCase
{
public function testReturnsConcreteClassesInNamespace(): void
{
$classes = ComposerClassFinder::directClassesInNamespace('Nuwave\\Lighthouse\\Cache');
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use fixtures here instead of real classes so we don't rely on implementation details?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, should make the tests more stable - seeing them fail while working on something related to cache would be weird.


$this->assertContains(CacheDirective::class, $classes);
$this->assertContains(CacheServiceProvider::class, $classes);
}

public function testFiltersInterfacesAndTraits(): void
{
$classes = ComposerClassFinder::directClassesInNamespace('Nuwave\\Lighthouse\\Cache');

$this->assertNotContains(CacheKeyAndTags::class, $classes, 'Interfaces must not be returned.');
}

public function testDoesNotRecurseIntoSubNamespaces(): void
{
$classes = ComposerClassFinder::directClassesInNamespace('Nuwave\\Lighthouse\\Support');

$this->assertContains(ComposerClassFinder::class, $classes);
$this->assertContains(Utils::class, $classes);
$this->assertContains(AppVersion::class, $classes);
$this->assertContains(DriverManager::class, $classes);

foreach ($classes as $class) {
$this->assertStringNotContainsString(
'Nuwave\\Lighthouse\\Support\\Contracts\\',
$class,
'Sub-namespace Contracts must not be recursed into.',
);
$this->assertStringNotContainsString(
'Nuwave\\Lighthouse\\Support\\Traits\\',
$class,
'Sub-namespace Traits must not be recursed into.',
);
}
}

public function testReturnsEmptyArrayForUnknownNamespace(): void
{
$this->assertSame([], ComposerClassFinder::directClassesInNamespace('Definitely\\Not\\A\\Real\\Namespace'));
}

public function testReturnsEmptyArrayForLeadingBackslash(): void
{
// Matches the strict behavior of `HaydenPierce\ClassFinder` in `STANDARD_MODE`,
// which does not normalize a leading backslash.
$this->assertSame([], ComposerClassFinder::directClassesInNamespace('\\Nuwave\\Lighthouse\\Cache'));
}

public function testTolerateTrailingBackslash(): void
{
$without = ComposerClassFinder::directClassesInNamespace('Nuwave\\Lighthouse\\Cache');
$with = ComposerClassFinder::directClassesInNamespace('Nuwave\\Lighthouse\\Cache\\');

sort($without);
sort($with);

$this->assertSame($without, $with);
}

public function testDiscoversClassesFromAutoloadDevPrefix(): void
{
$classes = ComposerClassFinder::directClassesInNamespace('Tests\\Console');

$this->assertContains(UnionDirective::class, $classes);
}
}
Loading