northrook/php-class-finder

Discover autoloadable PHP classes, interfaces, and traits from directories

Maintainers

Package info

codeberg.org/northrook/php-class-finder/

Issues

pkg:composer/northrook/php-class-finder

Transparency log

Statistics

Installs: 28

Dependents: 3

Suggesters: 0

dev-main 2026-09-02 13:01 UTC

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, and readonly classes
  • Interfaces and traits
  • Symbols with attributes on preceding lines, including multiline #[...]

What gets skipped

  • Hidden files and directories (names starting with .)
  • Non-.php files
  • 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, or die
  • Namespaces listed in excludeNamespaces() (defaults to composer)
  • 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.

MemberDescription
$lastScanMost 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).

MethodDescription
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().

CaseValueDescription
StandardClassclassNon-abstract, non-final class
AbstractClassabstract-classAbstract class
FinalClassfinal-classFinal class
InterfaceinterfaceInterface
TraittraitTrait

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:

  1. Parent classes (base to derived)
  2. Traits (including nested use)
  3. Interfaces (including extended parents)
  4. 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.

MemberDescription
$attributeThe underlying ReflectionAttribute
$targetClassFQCN of the symbol that declared the attribute
$targetDefinitionKind of the declaring symbol
$nameAttribute class name
__invoke()Instantiate the attribute object

Development

composer test
composer phpstan

License

BSD-3-Clause. See LICENSE.