diff --git a/docs/usage.md b/docs/usage.md index 3e0f36f47..06e2e7dac 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -94,6 +94,12 @@ within the `Reflector`s. The library comes bundled with the following * `AggregateSourceLocator` - a combination of multiple `SourceLocator`s which are hunted through in the given order to locate the source. + * `FileIteratorSourceLocator` - iterates all files in a given iterator + containing `SplFileInfo` instances. + + * `DirectoriesSourceLocator` - iterates over all `.php` files in a list of + directories, and all their descendants. + A `SourceLocator` is a callable, which when invoked must be given an `Identifier` (which describes a class/function/etc.). The `SourceLocator` should be written so that it returns a `Reflection` object directly. @@ -172,6 +178,39 @@ $reflector = new ClassReflector(new SingleFileSourceLocator('path/to/file.php')) $classes = $reflector->getAllClasses(); ``` +### Fetch reflections of all the classes in a directory + +```php +getAllClasses(); +``` + +### Fetch reflections of all the classes in a directory (including sub directories) + +```php +getAllClasses(); +``` + +### Fetch reflections of all the classes in multiple directories (including sub directories) + +```php +getAllClasses(); +``` + + ## Reflecting Functions The `FunctionReflector` is used to create Better Reflection `ReflectionFunction` diff --git a/src/SourceLocator/Exception/InvalidDirectory.php b/src/SourceLocator/Exception/InvalidDirectory.php new file mode 100644 index 000000000..dc0fd6e85 --- /dev/null +++ b/src/SourceLocator/Exception/InvalidDirectory.php @@ -0,0 +1,33 @@ +aggregateSourceLocator = new AggregateSourceLocator(array_values(array_map( + function ($directory) { + if (! is_string($directory)) { + throw InvalidDirectory::fromNonStringValue($directory); + } + + if (! is_dir($directory)) { + throw InvalidDirectory::fromNonDirectory($directory); + } + + return new FileIteratorSourceLocator(new RecursiveIteratorIterator(new RecursiveDirectoryIterator( + $directory, + RecursiveDirectoryIterator::SKIP_DOTS + ))); + }, + $directories + ))); + } + + /** + * {@inheritDoc} + */ + public function locateIdentifier(Reflector $reflector, Identifier $identifier) + { + return $this->aggregateSourceLocator->locateIdentifier($reflector, $identifier); + } + + /** + * {@inheritDoc} + */ + public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType) + { + return $this->aggregateSourceLocator->locateIdentifiersByType($reflector, $identifierType); + } +} diff --git a/src/SourceLocator/Type/FileIteratorSourceLocator.php b/src/SourceLocator/Type/FileIteratorSourceLocator.php new file mode 100644 index 000000000..63824a4e4 --- /dev/null +++ b/src/SourceLocator/Type/FileIteratorSourceLocator.php @@ -0,0 +1,74 @@ +fileSystemIterator = $fileInfoIterator; + } + + /** + * @return AggregateSourceLocator + */ + private function getAggregatedSourceLocator() + { + return $this->aggregateSourceLocator ? + $this->aggregateSourceLocator : new AggregateSourceLocator(array_values(array_filter(array_map( + function (\SplFileInfo $item) { + if (! ($item->isFile() && pathinfo($item->getRealPath(), \PATHINFO_EXTENSION) === 'php')) { + return null; + } + + return new SingleFileSourceLocator($item->getRealPath()); + }, + iterator_to_array($this->fileSystemIterator) + )))); + } + + /** + * {@inheritDoc} + */ + public function locateIdentifier(Reflector $reflector, Identifier $identifier) + { + return $this->getAggregatedSourceLocator()->locateIdentifier($reflector, $identifier); + } + + /** + * {@inheritDoc} + */ + public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType) + { + return $this->getAggregatedSourceLocator()->locateIdentifiersByType($reflector, $identifierType); + } +} diff --git a/test/unit/Assets/DirectoryScannerAssets/Bar/Empty.php b/test/unit/Assets/DirectoryScannerAssets/Bar/Empty.php new file mode 100644 index 000000000..03622f520 --- /dev/null +++ b/test/unit/Assets/DirectoryScannerAssets/Bar/Empty.php @@ -0,0 +1,4 @@ +getMessage()); + } + + /** + * @return string[][]|mixed[][] + */ + public function nonStringValuesProvider() + { + return [ + ['Expected string, stdClass given', new \stdClass()], + ['Expected string, boolean given', true], + ['Expected string, NULL given', null], + ['Expected string, integer given', 100], + ['Expected string, double given', 100.35], + ['Expected string, array given', []], + ]; + } + + public function testFromNonDirectoryWithNonExistingPath() + { + $directory = uniqid(sys_get_temp_dir() . 'non-existing', true); + $exception = InvalidDirectory::fromNonDirectory($directory); + + self::assertInstanceOf(InvalidDirectory::class, $exception); + self::assertSame(sprintf('"%s" does not exists', $directory), $exception->getMessage()); + } + + public function testFromNonDirectoryWithFile() + { + $exception = InvalidDirectory::fromNonDirectory(__FILE__); + + self::assertInstanceOf(InvalidDirectory::class, $exception); + self::assertSame(sprintf('"%s" must be a directory, not a file', __FILE__), $exception->getMessage()); + } +} diff --git a/test/unit/SourceLocator/Exception/InvalidFileInfoTest.php b/test/unit/SourceLocator/Exception/InvalidFileInfoTest.php new file mode 100644 index 000000000..db1265f78 --- /dev/null +++ b/test/unit/SourceLocator/Exception/InvalidFileInfoTest.php @@ -0,0 +1,42 @@ +getMessage()); + } + + /** + * @return string[][]|mixed[][] + */ + public function nonSplFileInfoProvider() + { + return [ + ['Expected an iterator of SplFileInfo instances, stdClass given instead', new \stdClass()], + ['Expected an iterator of SplFileInfo instances, boolean given instead', true], + ['Expected an iterator of SplFileInfo instances, NULL given instead', null], + ['Expected an iterator of SplFileInfo instances, integer given instead', 100], + ['Expected an iterator of SplFileInfo instances, double given instead', 100.35], + ['Expected an iterator of SplFileInfo instances, array given instead', []], + ]; + } +} diff --git a/test/unit/SourceLocator/Type/DirectoriesSourceLocatorTest.php b/test/unit/SourceLocator/Type/DirectoriesSourceLocatorTest.php new file mode 100644 index 000000000..6bae739ea --- /dev/null +++ b/test/unit/SourceLocator/Type/DirectoriesSourceLocatorTest.php @@ -0,0 +1,103 @@ +sourceLocator = new DirectoriesSourceLocator([ + __DIR__ . '/../../Assets/DirectoryScannerAssets', + __DIR__ . '/../../Assets/DirectoryScannerAssetsFoo', + ]); + } + + public function testScanDirectoryClasses() + { + $classes = $this->sourceLocator->locateIdentifiersByType( + new ClassReflector($this->sourceLocator), + new IdentifierType(IdentifierType::IDENTIFIER_CLASS) + ); + + self::assertCount(4, $classes); + + $classNames = array_map( + function (ReflectionClass $reflectionClass) { + return $reflectionClass->getName(); + }, + $classes + ); + + sort($classNames); + + self::assertEquals(DirectoryScannerAssetsFoo\Bar\FooBar::class, $classNames[0]); + self::assertEquals(DirectoryScannerAssetsFoo\Foo::class, $classNames[1]); + self::assertEquals(DirectoryScannerAssets\Bar\FooBar::class, $classNames[2]); + self::assertEquals(DirectoryScannerAssets\Foo::class, $classNames[3]); + } + + public function testLocateIdentifier() + { + $class = $this->sourceLocator->locateIdentifier( + new ClassReflector($this->sourceLocator), + new Identifier( + DirectoryScannerAssets\Bar\FooBar::class, + new IdentifierType(IdentifierType::IDENTIFIER_CLASS) + ) + ); + + self::assertInstanceOf(ReflectionClass::class, $class); + self::assertSame(DirectoryScannerAssets\Bar\FooBar::class, $class->getName()); + } + + /** + * @dataProvider invalidDirectoriesProvider + * + * @param array $directories + */ + public function testInvalidDirectory(array $directories) + { + $this->expectException(InvalidDirectory::class); + + new DirectoriesSourceLocator($directories); + } + + public function invalidDirectoriesProvider() + { + $validDir = __DIR__ . '/../../Assets/DirectoryScannerAssets'; + + return [ + [[__DIR__ . '/' . uniqid('nonExisting', true)]], + [[__FILE__]], + [[1]], + [[1.23]], + [[true]], + [[new \stdClass()]], + [[null]], + [[$validDir, __DIR__ . '/' . uniqid('nonExisting', true)]], + [[$validDir, __FILE__]], + [[$validDir, 1]], + [[$validDir, 1.23]], + [[$validDir, true]], + [[$validDir, new \stdClass()]], + [[$validDir, null]], + ]; + } +} diff --git a/test/unit/SourceLocator/Type/FileIteratorSourceLocatorTest.php b/test/unit/SourceLocator/Type/FileIteratorSourceLocatorTest.php new file mode 100644 index 000000000..460e444d6 --- /dev/null +++ b/test/unit/SourceLocator/Type/FileIteratorSourceLocatorTest.php @@ -0,0 +1,72 @@ +sourceLocator = new FileIteratorSourceLocator( + new RecursiveIteratorIterator(new RecursiveDirectoryIterator( + __DIR__ . '/../../Assets/DirectoryScannerAssets', + RecursiveDirectoryIterator::SKIP_DOTS + )) + ); + } + + public function testScanDirectoryClasses() + { + $classes = $this->sourceLocator->locateIdentifiersByType( + new ClassReflector($this->sourceLocator), + new IdentifierType(IdentifierType::IDENTIFIER_CLASS) + ); + + self::assertCount(2, $classes); + + $classNames = array_map( + function (ReflectionClass $reflectionClass) { + return $reflectionClass->getName(); + }, + $classes + ); + + sort($classNames); + + self::assertEquals(DirectoryScannerAssets\Bar\FooBar::class, $classNames[0]); + self::assertEquals(DirectoryScannerAssets\Foo::class, $classNames[1]); + } + + public function testLocateIdentifier() + { + $class = $this->sourceLocator->locateIdentifier( + new ClassReflector($this->sourceLocator), + new Identifier( + DirectoryScannerAssets\Bar\FooBar::class, + new IdentifierType(IdentifierType::IDENTIFIER_CLASS) + ) + ); + + self::assertInstanceOf(ReflectionClass::class, $class); + self::assertSame(DirectoryScannerAssets\Bar\FooBar::class, $class->getName()); + } +}