northrook / php-class-finder
Discover autoloadable PHP classes, interfaces, and traits from directories
Requires
- php: >=8.5
- northrook/core-contracts: dev-main
Requires (Dev)
- northrook/php-cs: dev-main
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^13
This package is auto-updated.
Last update: 2026-09-02 12:02:13 UTC
README
Discover autoloadable PHP classes, interfaces, and traits from directories.
- Requires PHP 8.5+
- No runtime dependencies
Installation
composer require northrook/php-class-finder
Quick start
Point the finder at your project root, scan one or more directories, then filter by attribute:
use Northrook\ClassFinder;
$finder = new ClassFinder(__DIR__);
$services = $finder
->scanDirectories('src/*')
->anyAttribute(App\Attribute\Service::class);
foreach ($services as $classInfo) {
$instance = $classInfo(); // instantiate via __invoke
}
Paths passed to scanDirectories() are relative to the root directory.
Append * for recursive scanning (src/*); omit it to scan only the immediate directory (src).
How it works
Walks .php files with a lightweight line parser, then keeps only symbols the autoloader can resolve.
Results are keyed by the absolute file path.
Does not consult Composer’s PSR-4 map; non-autoloadable matches are dropped.
Intended for cold-path work: DI bootstrap, route discovery, and similar.
What gets included
- Namespaced and global classes, including
final,abstract, andreadonlyclasses - Interfaces and traits
- Symbols with attributes on preceding lines, including multiline
#[...]
What gets skipped
- Hidden files and directories (names starting with
.) - Non-
.phpfiles - Enums
- Files with no class, interface, or trait definition
- A second declaration in the same file (only the first is read)
- Symbols that are not autoloadable
- Code after an early
return,exit, ordie - Namespaces listed in
excludeNamespaces()(defaults tocomposer) - Subdirectories listed in
excludeSubdirectories()(default none)
API
ClassFinder
Configure the root path and exclusions, then scan.
$finder = new ClassFinder('/path/to/project');
$finder->excludeNamespaces('Vendor\\Generated', 'Tests');
$finder->excludeSubdirectories('tests', 'vendor', 'src/Generated');
$scan = $finder->scanDirectories('src/*', 'vendor/*', 'config');
Plain subdirectory names match any directory basename under a recursive scan.
Paths with / are resolved against the project root. Excludes do not block an
explicit scanDirectories() target.
| Member | Description |
|---|---|
$lastScan | Most recent ClassScan, or null |
excludeNamespaces(...) | Replace excluded namespaces; no arguments resets to ['composer'] |
excludeSubdirectories(...) | Replace excluded directories; no arguments resets to [] |
scanDirectories(...) | Scan directories relative to the root; returns ClassScan |
Root path validation happens when scanning, not in the constructor.
ClassScan
Immutable scan result. Iterable and countable; values are ClassInfo instances.
use Northrook\ClassFinder\Definition;
$scan = $finder->scanDirectories('src/*');
$scan->count();
$scan->getArray(); // [filePath => ClassInfo]
$scan->classes(); // all class kinds
$scan->abstractClasses(); // abstract classes only
$scan->finalClasses(); // final classes only
$scan->ofKind(Definition::StandardClass); // non-abstract, non-final classes
$scan->interfaces(); // interfaces only
$scan->traits(); // traits only
$scan->ofKind(Definition::Interface, Definition::Trait); // filter by kind
$scan->anyAttribute(Route::class, Autowire::class); // symbols with at least one match
$scan->withAttributes(Autowire::class, LoggerInterface::class); // symbols with every attribute
anyAttribute() and withAttributes() match via IS_INSTANCEOF (attribute
subclasses count as the parent type). anyAttribute() with no arguments returns
every symbol that has at least one attribute.
Both methods resolve attributes from the full composition tree of each symbol (see Attribute composition).
| Method | Description |
|---|---|
classes() | Standard, abstract, and final classes |
abstractClasses() | Abstract classes only |
finalClasses() | Final classes only |
interfaces() | Interfaces only |
traits() | Traits only |
ofKind(...) | Symbols matching any of the given kinds |
anyAttribute(...) | Symbols with at least one matching attribute |
withAttributes(...) | Symbols with every matching attribute |
ClassInfo
Metadata and reflection helpers for a single class, interface, or trait.
use Northrook\ClassFinder\ClassInfo;
use Northrook\ClassFinder\Definition;
$info = ClassInfo::from(SomeService::class);
$info->fqcn; // fully qualified name
$info->basename; // short name
$info->namespace; // namespace without short name
$info->file; // absolute path to the defining file
$info->kind; // Definition::StandardClass, ::AbstractClass, ::FinalClass, ::Interface, or ::Trait
$info->reflection; // lazy ReflectionClass
$info->isClass(); // true for standard, abstract, and final classes
$info->isAbstractClass();
$info->isFinalClass();
$info->isInterface();
$info->isTrait();
$info->hasAttribute(Service::class); // IS_INSTANCEOF, walks composition tree
$info->getAttributes(Service::class); // same matching, returns ReflectionAttribute[]
$info->attributes; // grouped ClassAttribute wrappers (see below)
$info(...$args); // instantiate the class (classes only)
ClassInfo::basename($class, 'strtolower');
ClassInfo::exists('Some\\Class');
__invoke() throws for interfaces, traits, and abstract classes.
Definition
Kind of a discovered symbol. Used by ClassInfo::$kind and ClassScan::ofKind().
| Case | Value | Description |
|---|---|---|
StandardClass | class | Non-abstract, non-final class |
AbstractClass | abstract-class | Abstract class |
FinalClass | final-class | Final class |
Interface | interface | Interface |
Trait | trait | Trait |
Definition::resolve(ReflectionClass) derives the kind from reflection.
Definition::classKinds() returns the three class variants for ClassScan::classes().
Attribute composition
Attribute lookup walks the full composition tree of a symbol:
- Parent classes (base to derived)
- Traits (including nested
use) - Interfaces (including extended parents)
- The symbol itself
Each source symbol is visited once. The same attribute type may appear more
than once when declared on different sources — for example a #[Service] on a
base class and again on a child. These duplicates are kept intentionally so
arguments can be compared or merged across the tree.
ClassInfo::$attributes exposes the full tree as ClassAttribute wrappers,
grouped by attribute class name:
foreach ($info->attributes[Service::class] ?? [] as $classAttribute) {
$classAttribute->name; // attribute FQCN
$classAttribute->targetClass; // class/interface/trait that declared it
$classAttribute->targetDefinition; // Definition of the declaring symbol
$instance = $classAttribute(); // instantiate the attribute object
}
getAttributes() and hasAttribute() use the same composition walk but return
plain ReflectionAttribute instances for convenience.
ClassAttribute
Wrapper around one ReflectionAttribute with provenance from the composition tree.
| Member | Description |
|---|---|
$attribute | The underlying ReflectionAttribute |
$targetClass | FQCN of the symbol that declared the attribute |
$targetDefinition | Kind of the declaring symbol |
$name | Attribute class name |
__invoke() | Instantiate the attribute object |
Development
composer test
composer phpstan
License
BSD-3-Clause. See LICENSE.