Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding methods for sequential processing and filtering of class reflections #1475

Open
wants to merge 10 commits into
base: 6.52.x
Choose a base branch
from
37 changes: 37 additions & 0 deletions demo/parsing-whole-directory/example3.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

# example of class iterator operation

require_once __DIR__ . '/../../vendor/autoload.php';

use Roave\BetterReflection\BetterReflection;
use Roave\BetterReflection\Reflector\DefaultReflector;
use Roave\BetterReflection\SourceLocator\Type\AggregateSourceLocator;
use Roave\BetterReflection\SourceLocator\Type\DirectoriesSourceLocator;
use Roave\BetterReflection\SourceLocator\Type\SourceFilter\FileSizeFilter;
use Roave\BetterReflection\SourceLocator\Type\SourceFilter\AggregateFilter;
use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceContainsFilter;

$directories = [__DIR__ . '/../../src', __DIR__ . '/../../demo'];

$sourceLocator = new AggregateSourceLocator([
new DirectoriesSourceLocator(
$directories,
(new BetterReflection())->astLocator()
),
]);

$reflector = new DefaultReflector($sourceLocator);

$classReflections = $reflector->iterateClasses(
new AggregateFilter(
new FileSizeFilter(10000),
new SourceContainsFilter(['class ReflectionMethod', 'class ReflectionClass'])
),
Comment on lines +27 to +30
Copy link
Member

Choose a reason for hiding this comment

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

From what I see, you attempted to design this code so that you could push a runtime filter down the stack, into the lookup operation, for example, to select all classes in a specific namespace.

I understand the rationale, but am a bit conflicted on the design, and the attempt to create a structured filter here, which is extremely risky due to the internals of source locators being squishy, whilst this interface being public.

I don't have a clear design/suggestion for you right now, but I think a first attempt should be done at creating a single FilteringSourceLocator that can wrap a $next one and apply filtering at that level, operating on a LocatedSource|null: that would make the use-case more specific, and would isolate the filtering change onto a single implementation.

);

print iterator_count($classReflections);

$classReflections = $reflector->iterateClasses(new SourceContainsFilter(['class MyClass']));

print iterator_count($classReflections);
59 changes: 40 additions & 19 deletions docs/usage.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Basic Usage

The starting point for creating a reflection class does not match the typical core reflection API. Instead of
instantiating a `new \ReflectionClass`, you must use the appropriate `\Roave\BetterReflection\Reflector\Reflector`
The starting point for creating a reflection class does not match the typical core reflection API. Instead of
instantiating a `new \ReflectionClass`, you must use the appropriate `\Roave\BetterReflection\Reflector\Reflector`
helper.

All `*Reflector` classes require a class that implements the `SourceLocator` interface as a dependency.

## Basic Reflection

Better Reflection is, in most cases, able to automatically reflect on classes by using a similar creation technique to
Better Reflection is, in most cases, able to automatically reflect on classes by using a similar creation technique to
PHP's internal reflection. However, this works on the basic assumption that whichever autoloader you are using will
attempt to load a file, and only one file, which should contain the class you are trying to reflect. For example, the
attempt to load a file, and only one file, which should contain the class you are trying to reflect. For example, the
autoloader that Composer provides will work with this technique.

```php
Expand All @@ -23,10 +23,10 @@ $classInfo = (new BetterReflection)
->reflectClass(\Foo\Bar\MyClass::class);
```

If this instantiation technique is not possible - for example, your autoloader does not load classes from file, then
If this instantiation technique is not possible - for example, your autoloader does not load classes from file, then
you *must* use `SourceLocator` creation.

> Fun fact... using the method described above actually uses a SourceLocator under the hood - it uses the
> Fun fact... using the method described above actually uses a SourceLocator under the hood - it uses the
`AutoloadSourceLocator`.

### Initialisers
Expand Down Expand Up @@ -60,20 +60,20 @@ ReflectionProperty::createFromInstance(new \ReflectionClass(\stdClass::class), '

## SourceLocators

Source locators are helpers that identify how to load code that can be used within the `Reflector`s. The library comes
Source locators are helpers that identify how to load code that can be used within the `Reflector`s. The library comes
bundled with the following `SourceLocator` classes:

* `ComposerSourceLocator` - you'll probably use this most of the time. This uses Composer's built-in autoloader to
* `ComposerSourceLocator` - you'll probably use this most of the time. This uses Composer's built-in autoloader to
locate a class and return the source.

* `SingleFileSourceLocator` - this locator loads the filename specified in the constructor.
* `StringSourceLocator` - pass a string as a constructor argument which will be used directly. Note that any

* `StringSourceLocator` - pass a string as a constructor argument which will be used directly. Note that any
references to filenames when using this locator will be `null` because no files are loaded.

* `AutoloadSourceLocator` - this is a little hacky, but works on the assumption that when a registered autoloader
* `AutoloadSourceLocator` - this is a little hacky, but works on the assumption that when a registered autoloader
identifies a file and attempts to open it, then that file will contain the class. Internally, it works by overriding
the `file://` protocol stream wrapper to grab the path of the file the autoloader is trying to locate. This source
the `file://` protocol stream wrapper to grab the path of the file the autoloader is trying to locate. This source
locator is used internally by the `ReflectionClass::createFromName` static constructor.

* `EvaledCodeSourceLocator` - used to perform reflection on code that is already loaded into memory using `eval()`
Expand All @@ -84,7 +84,7 @@ bundled with the following `SourceLocator` classes:

* `ClosureSourceLocator` - used to perform reflection on a closure.

* `AggregateSourceLocator` - a combination of multiple `SourceLocator`s which are hunted through in the given order to
* `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.
Expand All @@ -94,7 +94,7 @@ bundled with the following `SourceLocator` classes:
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.

> Note that using `EvaledCodeSourceLocator` and `PhpInternalSourceLocator` will result in specific types of
> Note that using `EvaledCodeSourceLocator` and `PhpInternalSourceLocator` will result in specific types of
`LocatedSource` within the reflection - namely `EvaledLocatedSource` and `InternalLocatedSource` respectively.

> Note that if you use a locator other than the default and the class you want to reflect extends a built-in PHP class (e.g. `\Exception`)
Expand All @@ -103,12 +103,12 @@ A `SourceLocator` is a callable, which when invoked must be given an `Identifier

## Reflecting Classes

The `Reflector` is used to create Better Reflection `ReflectionClass` instances. You may pass it any
The `Reflector` is used to create Better Reflection `ReflectionClass` instances. You may pass it any
`SourceLocator` to reflect on any class that can be located using the given that `SourceLocator`.

### Using the AutoloadSourceLocator

There is no need to use the `AutoloadSourceLocator` directly. Use the static constructors for `ReflectionClass`
There is no need to use the `AutoloadSourceLocator` directly. Use the static constructors for `ReflectionClass`
and `ReflectionFunction`:

```php
Expand Down Expand Up @@ -237,10 +237,31 @@ $reflector = new DefaultReflector($directoriesSourceLocator);
$classes = $reflector->reflectAllClasses();
```

### Iterate filtered reflections in a directory

```php
<?php

use Roave\BetterReflection\BetterReflection;
use Roave\BetterReflection\Reflector\DefaultReflector;
use Roave\BetterReflection\SourceLocator\Type\DirectoriesSourceLocator;

$astLocator = (new BetterReflection())->astLocator();

$directoriesSourceLocator = new DirectoriesSourceLocator(['path/to/directory1'], $astLocator);

$reflector = new DefaultReflector($directoriesSourceLocator);

$generator = $reflector->iterateClasses(new SourceContainsFilter(['some substring']));

foreach ($generator as $reflection) {
$reflection->getName();
}
```

## Reflecting Functions

The `Reflector` is used to create Better Reflection `ReflectionFunction` instances. You may pass it any
The `Reflector` is used to create Better Reflection `ReflectionFunction` instances. You may pass it any
`SourceLocator` to reflect on any class that can be located using the given `SourceLocator`.

### Using the AutoloadSourceLocator
Expand Down Expand Up @@ -277,5 +298,5 @@ $myClosure = function () {
$functionInfo = ReflectionFunction::createFromClosure($myClosure);
```

> Note that when you reflect on a closure, in order to match the core reflection API, the function "short" name will be
> Note that when you reflect on a closure, in order to match the core reflection API, the function "short" name will be
just `{closure}`.
20 changes: 19 additions & 1 deletion src/Reflector/DefaultReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

namespace Roave\BetterReflection\Reflector;

use Generator;
use Roave\BetterReflection\Identifier\Identifier;
use Roave\BetterReflection\Identifier\IdentifierType;
use Roave\BetterReflection\Reflection\ReflectionClass;
use Roave\BetterReflection\Reflection\ReflectionConstant;
use Roave\BetterReflection\Reflection\ReflectionFunction;
use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound;
use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceFilter;
use Roave\BetterReflection\SourceLocator\Type\SourceLocator;

use function assert;

final class DefaultReflector implements Reflector
Expand Down Expand Up @@ -56,6 +57,23 @@ public function reflectAllClasses(): iterable
return $allClasses;
}

/**
* Iterate classes available in the scope specified by the SourceLocator.
*
* @return Generator<ReflectionClass>
Copy link
Member

Choose a reason for hiding this comment

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

Overall, I'd say that we can change the default reflectAllClasses() API to return an iterable<ReflectionClass>, rather than adding new API here.

Yes, it's a breaking change, but it's a good one to have

*/
public function iterateClasses(?SourceFilter $filter = null): Generator
{
/** @var Generator<ReflectionClass> $allClasses */
$allClasses = $this->sourceLocator->iterateIdentifiersByType(
$this,
new IdentifierType(IdentifierType::IDENTIFIER_CLASS),
$filter,
);

yield from $allClasses;
}
Comment on lines +65 to +75
Copy link
Member

Choose a reason for hiding this comment

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

I don't think SourceFilter should be given at call-time: it should be an implementation detail of the source locator only, IMO.


/**
* Create a ReflectionFunction for the specified $functionName.
*
Expand Down
29 changes: 29 additions & 0 deletions src/SourceLocator/Type/AbstractSourceLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Roave\BetterReflection\SourceLocator\Type;

use Generator;
use Roave\BetterReflection\Identifier\Identifier;
use Roave\BetterReflection\Identifier\IdentifierType;
use Roave\BetterReflection\Reflection\Reflection;
Expand All @@ -12,6 +13,7 @@
use Roave\BetterReflection\SourceLocator\Ast\Exception\ParseToAstFailure;
use Roave\BetterReflection\SourceLocator\Ast\Locator as AstLocator;
use Roave\BetterReflection\SourceLocator\Located\LocatedSource;
use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceFilter;

abstract class AbstractSourceLocator implements SourceLocator
{
Expand Down Expand Up @@ -68,4 +70,31 @@ final public function locateIdentifiersByType(Reflector $reflector, IdentifierTy
$identifierType,
);
}

/**
* {@inheritDoc}
*
* @throws ParseToAstFailure
*/
public function iterateIdentifiersByType(
Reflector $reflector,
IdentifierType $identifierType,
?SourceFilter $sourceFilter,
Copy link
Member

Choose a reason for hiding this comment

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

Mentioned elsewhere: this is IMO something that shouldn't be given at call-time, but should be instead intrinsic of the way you are looking at a codebase (effectively, a FilteredSourceLocator)

): Generator {
$locatedSource = $this->createLocatedSource(new Identifier(Identifier::WILDCARD, $identifierType));

if (!$locatedSource || $sourceFilter?->isAllowed(
$locatedSource->getSource(),
$locatedSource->getName(),
$locatedSource->getFileName(),
) === false) {
return;
}

yield from $this->astLocator->findReflectionsOfType(
$reflector,
$locatedSource,
$identifierType,
);
}
}
16 changes: 16 additions & 0 deletions src/SourceLocator/Type/AggregateSourceLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

namespace Roave\BetterReflection\SourceLocator\Type;

use Generator;
use Roave\BetterReflection\Identifier\Identifier;
use Roave\BetterReflection\Identifier\IdentifierType;
use Roave\BetterReflection\Reflection\Reflection;
use Roave\BetterReflection\Reflector\Reflector;
use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceFilter;

use function array_map;
use function array_merge;
Expand Down Expand Up @@ -42,4 +44,18 @@ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $id
...array_map(static fn (SourceLocator $sourceLocator): array => $sourceLocator->locateIdentifiersByType($reflector, $identifierType), $this->sourceLocators),
);
}

/**
* {@inheritDoc}
*/
public function iterateIdentifiersByType(
Reflector $reflector,
IdentifierType $identifierType,
?SourceFilter $sourceFilter,
): Generator
{
foreach ($this->sourceLocators as $sourceLocator) {
yield from $sourceLocator->iterateIdentifiersByType($reflector, $identifierType, $sourceFilter);
}
}
}
34 changes: 34 additions & 0 deletions src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Roave\BetterReflection\SourceLocator\Type;

use Generator;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\NodeTraverser;
Expand All @@ -23,6 +24,7 @@
use Roave\BetterReflection\SourceLocator\Exception\TwoAnonymousClassesOnSameLine;
use Roave\BetterReflection\SourceLocator\FileChecker;
use Roave\BetterReflection\SourceLocator\Located\AnonymousLocatedSource;
use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceFilter;
use Roave\BetterReflection\Util\FileHelper;

use function array_filter;
Expand Down Expand Up @@ -60,6 +62,38 @@
return array_filter([$this->getReflectionClass($reflector, $identifierType)]);
}

/**
* {@inheritDoc}
*
* @throws ParseToAstFailure
*/
public function iterateIdentifiersByType(
Reflector $reflector,
IdentifierType $identifierType,
?SourceFilter $sourceFilter
): Generator
{
$reflections = $this->locateIdentifiersByType($reflector, $identifierType);
if (! $reflections) {
return;
}

foreach ($reflections as $reflection) {
$locatedSource = $reflection->getLocatedSource();

Check failure on line 82 in src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

MixedAssignment

src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php:82:13: MixedAssignment: Unable to determine the type that $locatedSource is being assigned to (see https://psalm.dev/032)

Check failure on line 82 in src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

UndefinedInterfaceMethod

src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php:82:43: UndefinedInterfaceMethod: Method Roave\BetterReflection\Reflection\Reflection::getLocatedSource does not exist (see https://psalm.dev/181)

Check failure on line 82 in src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php

View workflow job for this annotation

GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)

Call to an undefined method Roave\BetterReflection\Reflection\Reflection::getLocatedSource().

Check failure on line 82 in src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php

View workflow job for this annotation

GitHub Actions / Static Analysis by PHPStan (locked, 8.4, ubuntu-latest)

Call to an undefined method Roave\BetterReflection\Reflection\Reflection::getLocatedSource().

Check failure on line 82 in src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php

View workflow job for this annotation

GitHub Actions / Static Analysis by PHPStan (locked, 8.2, ubuntu-latest)

Call to an undefined method Roave\BetterReflection\Reflection\Reflection::getLocatedSource().
$isAllowed = $sourceFilter?->isAllowed(
$locatedSource->getSource(),

Check failure on line 84 in src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

MixedArgument

src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php:84:17: MixedArgument: Argument 1 of Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceFilter::isAllowed cannot be mixed, expecting string (see https://psalm.dev/030)

Check failure on line 84 in src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

MixedMethodCall

src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php:84:33: MixedMethodCall: Cannot determine the type of $locatedSource when calling method getSource (see https://psalm.dev/015)
$locatedSource->getName(),

Check failure on line 85 in src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

MixedArgument

src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php:85:17: MixedArgument: Argument 2 of Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceFilter::isAllowed cannot be mixed, expecting null|string (see https://psalm.dev/030)

Check failure on line 85 in src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

MixedMethodCall

src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php:85:33: MixedMethodCall: Cannot determine the type of $locatedSource when calling method getName (see https://psalm.dev/015)
$locatedSource->getFileName(),

Check failure on line 86 in src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

MixedArgument

src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php:86:17: MixedArgument: Argument 3 of Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceFilter::isAllowed cannot be mixed, expecting null|string (see https://psalm.dev/030)

Check failure on line 86 in src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

MixedMethodCall

src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php:86:33: MixedMethodCall: Cannot determine the type of $locatedSource when calling method getFileName (see https://psalm.dev/015)
);

if ($isAllowed === false) {
continue;
}

yield $reflection;
}
}

private function getReflectionClass(Reflector $reflector, IdentifierType $identifierType): ReflectionClass|null
{
if (! $identifierType->isClass()) {
Expand Down
Loading
Loading