componenta / class-finder
Lazy class discovery with composable filters and listener notification
Requires
- php: ^8.4
- componenta/arrayable: ^1.0
- componenta/config: ^1.0
- componenta/filter: ^1.0
- componenta/iterator: ^1.0
- componenta/tokenizer: ^1.0
- psr/container: ^2.0
- psr/log: ^3.0
- symfony/finder: ^7.0 || ^8.0
Requires (Dev)
- pestphp/pest: ^4.0
- phpunit/phpunit: ^12.0
README
Lazy PHP declaration discovery with composable filters and listener notification.
ClassFinder scans PHP files, extracts named class/interface/trait/enum declarations
through componenta/tokenizer, applies filters, and returns a replayable iterator of
ClassInfo metadata.
Installation
composer require componenta/class-finder
Requirements
- PHP 8.4+
symfony/findercomponenta/tokenizercomponenta/filtercomponenta/arrayablecomponenta/iteratorpsr/containerpsr/log
Related Packages
| Package | Why it matters here |
|---|---|
componenta/tokenizer |
Parses PHP source and returns ClassInfo declarations. |
symfony/finder |
Walks directories and selects PHP files. |
componenta/iterator |
Provides replayable iteration over discovered declarations. |
componenta/filter |
Provides composable filters used by the finder. |
componenta/app and *-app packages |
Run class discovery while building application cache. |
Quick Start
use Componenta\ClassFinder\ClassFinder; use Componenta\ClassFinder\Filter\InstantiableFilter; use Componenta\ClassFinder\Filter\PatternFilter; $finder = new ClassFinder([ PatternFilter::endsWith('Controller'), new InstantiableFilter(), ]); $controllers = $finder->find(__DIR__ . '/src', exclude: ['tests']); foreach ($controllers as $file => $classInfo) { echo $classInfo->fullyQualifiedName . PHP_EOL; }
Search Modes
find() accepts the tokenizer search bitmask directly:
use Componenta\Tokenizer\TokenizerInterface; $classes = $finder->find('src/', mode: TokenizerInterface::SEARCH_CLASSES); $contracts = $finder->find( 'src/', mode: TokenizerInterface::SEARCH_INTERFACES | TokenizerInterface::SEARCH_TRAITS, );
Search mode is a per-call argument. It is not a DI configuration key.
ClassIterator
ClassFinder::find() returns ClassIteratorInterface: lazy, replayable,
countable, arrayable, and filterable.
$classes = $finder->find('src/'); $classes->count(); // forces traversal once and caches the count $classes->toArray(); // list<ClassInfo> $filtered = $classes->withFilter(PatternFilter::namespace('App\\Http'));
The iterator caches traversed declarations so it can be iterated more than once.
Pattern Filters
PatternFilter matches ClassInfo metadata without reflection.
use Componenta\ClassFinder\Filter\PatternFilter; new PatternFilter('*Controller'); // class name suffix new PatternFilter('User*'); // class name prefix new PatternFilter('App\\User'); // exact fully-qualified class name new PatternFilter('*\\Api\\*Controller'); // fully-qualified wildcard pattern PatternFilter::exactMatch('UserController'); PatternFilter::namespace('App\\Http'); // namespace and children PatternFilter::exactNamespace('App\\Http\\Admin'); // exact namespace only PatternFilter::exactFqn('App\\Http\\UserController'); PatternFilter::fqn('App\\*\\*Controller'); PatternFilter::in(['UserController', 'PostController']);
Use exactNamespace() when the input is a namespace without wildcard. A bare
string containing \ is treated as a fully-qualified class name or FQN pattern.
Reflection Filters
Some filters require the declaration to be loaded because they use
ClassInfo::$reflector:
AttributeSearchFilterAttributePatternFilterAnyAttributeFilterHasAnyAttributesFilterImplementsFilterImplementsAnyFilterSubclassFilter
These filters are appropriate when scanned classes are autoloadable. For pure
source inspection of unloaded files, prefer metadata-only filters such as
PatternFilter, InstantiableFilter, IsAbstractFilter, and IsFinalFilter.
Attribute Filters
use Componenta\ClassFinder\Filter\AttributePatternFilter; use Componenta\ClassFinder\Filter\AttributeSearchFilter; use Componenta\ClassFinder\Filter\AnyAttributeFilter; AttributeSearchFilter::hasAttribute(Route::class); AttributeSearchFilter::hasAnyAttribute([Route::class, Command::class]); AttributeSearchFilter::hasAllAttributes([Cache::class, Validate::class]); AttributeSearchFilter::hasAttribute(Inject::class, deepSearch: true); new AttributePatternFilter('*Attribute', deepSearch: true); AttributePatternFilter::attributePrefix('App\\Attribute\\'); new AnyAttributeFilter([Route::class, Command::class], deepSearch: true);
deepSearch: true also checks methods, properties, and constants.
Listeners
Listeners are notified for each accepted declaration. Finalizable listeners are finalized after scanning, even when no declarations were found.
use Componenta\ClassFinder\ClassListenerInterface; use Componenta\ClassFinder\FinalizableListenerInterface; use Componenta\Tokenizer\ClassInfo; final class RouteCollector implements FinalizableListenerInterface { /** @var list<class-string> */ private array $routes = []; public function handle(ClassInfo $info): void { if ($info->reflector->getAttributes(Route::class) !== []) { $this->routes[] = $info->fullyQualifiedName; } } public function finalize(): void { // Build final registry or cache. } }
ClassListenerNotifier snapshots the provider's listeners once per notify()
call, so the same listener instances receive handle() and finalize().
Compile Integration
Packages that collect metadata through listeners can expose compilers without depending on an application runner:
use Componenta\ClassFinder\Compile\CompileResult; use Componenta\ClassFinder\Compile\ListenerCompilerInterface; final class RouteCollectorCompiler implements ListenerCompilerInterface { public function supports(object $listener): bool { return $listener instanceof RouteCollector; } public function compile(object $listener, string $cacheDir): CompileResult { return CompileResult::filesOnly([ $cacheDir . '/routes.cache.php' => '<?php return [];', ]); } }
Register compiler class names under
Componenta\ClassFinder\Compile\ConfigKey::LISTENER_COMPILERS. The host application
decides when discovery runs and where sidecar files are written.
Container Integration
Register the package provider in a PSR-11 compatible container:
$config = (new Componenta\ClassFinder\ConfigProvider())();
Runtime configuration keys are defined in Componenta\ClassFinder\ConfigKey:
| Constant | Value | Description |
|---|---|---|
ConfigKey::FILTERS |
Componenta\ClassFinder:filters |
Default FilterInterface instances for ClassFinderFactory. |
ConfigKey::LISTENERS |
Componenta\ClassFinder:listeners |
Listener service ids or ClassListenerInterface instances. |
Listener config is fail-fast: every entry must be a listener instance or a
service id string resolving to ClassListenerInterface.
License
MIT