kariricode / class-discovery
PHP 8.4+ class discovery component with attribute scanning, token-based parsing, multi-tier caching, and dependency analysis. Part of the KaririCode Framework ecosystem.
Package info
github.com/KaririCode-Framework/kariricode-classdiscovery
pkg:composer/kariricode/class-discovery
Requires
- php: ^8.4
Suggests
- kariricode/cache: For PSR-16 cache integration via CacheBridge
- kariricode/configurator: For environment-aware discovery configuration
- kariricode/dependency-injection: For PSR-11 container integration
This package is auto-updated.
Last update: 2026-03-04 18:16:03 UTC
README
PHP 8.4+ class discovery — token-based scanning, attribute automation, multi-tier caching and dependency analysis.
Discover controllers, services, and event listeners automatically — without loading a single class.
Installation · Quick Start · Use Cases · Scanners · Caching · CI Integration
The Problem
Modern PHP applications rely on convention over configuration — routes registered from #[Route], services injected from #[Service], listeners wired from #[EventListener]. Without a discovery engine, teams end up writing this manually:
// routes.php — manually maintained, drifts over time Router::get('/users', [UserController::class, 'index']); Router::post('/users', [UserController::class, 'store']); Router::get('/products', [ProductController::class, 'index']); // ... 300 more lines
And for services:
// bootstrap.php — duplicated, error-prone $container->bind(MailerService::class, MailerService::class); $container->bind(PaymentService::class, PaymentService::class); // ... one entry per class forever
The Solution
composer require kariricode/class-discovery
use KaririCode\ClassDiscovery\Filter\AttributeFilter; use KaririCode\ClassDiscovery\Scanner\{ComposerNamespaceResolver, FileScanner}; $scanner = new FileScanner(new ComposerNamespaceResolver()); $scanner->addFilter(new AttributeFilter(Route::class)); // Discovers and returns all #[Route]-annotated controllers — instantly $result = $scanner->scan(['src/Controller']);
One scan replaces hundreds of manual registrations. Results are immutable, cacheable, and resolved in 30–80ms cold / <3ms warm.
Requirements
| Requirement | Version |
|---|---|
| PHP | 8.4 or higher |
| Composer | 2.x |
Installation
composer require kariricode/class-discovery
Optional integrations:
composer require kariricode/cache # PSR-16 cache backend composer require kariricode/configurator # Environment-aware configuration composer require kariricode/dependency-injection # PSR-11 container
Quick Start
1 — Discover all classes
use KaririCode\ClassDiscovery\Scanner\{ComposerNamespaceResolver, FileScanner}; $scanner = new FileScanner(new ComposerNamespaceResolver()); $result = $scanner->scan(['src/']); foreach ($result as $fqcn => $metadata) { echo "{$fqcn} — {$metadata->filePath}\n"; } echo "Found " . $result->count() . " classes in " . round($result->getScanDuration() * 1000, 1) . "ms\n";
2 — Filter by attribute
use KaririCode\ClassDiscovery\Filter\AttributeFilter; $scanner->addFilter(new AttributeFilter(Route::class)); $result = $scanner->scan(['src/Controller']);
3 — Enable caching
use KaririCode\ClassDiscovery\Cache\{ChainCacheStrategy, FileCacheStrategy, MemoryCacheStrategy}; $cache = new ChainCacheStrategy( new MemoryCacheStrategy(), new FileCacheStrategy('/var/cache/discovery'), ); $scanner->setCacheStrategy($cache); $result = $scanner->scan(['src/']); // cold: ~60ms · warm: <2ms
Real-world Use Cases
HTTP Router — auto-register controllers from #[Route]
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] final readonly class Route { public function __construct( public readonly string $path, public readonly string $method = 'GET', ) {} } // Discovery $scanner = new FileScanner(new ComposerNamespaceResolver()); $scanner->addFilter(new AttributeFilter(Route::class)); $result = $scanner->scan(['src/Controller']); foreach ($result as $fqcn => $metadata) { $router->mount($fqcn); // class-level #[Route] foreach ($metadata->methods as $method) { if ($method->hasAttribute('Route')) { // Use ReflectionScanner to read path / method values } } }
DI Container — auto-register services from #[Service]
$scanner = new FileScanner(new ComposerNamespaceResolver()); $scanner->addFilter(new AttributeFilter(Service::class)); $scanner->setCacheStrategy($cache); // warm: <2ms $result = $scanner->scan(['src/Service', 'src/Repository']); foreach ($result as $fqcn => $metadata) { $container->bind($fqcn, $fqcn); // zero manual registration }
Event Dispatcher — auto-wire listeners from #[EventListener]
use KaririCode\ClassDiscovery\Scanner\ReflectionScanner; // ReflectionScanner reads actual attribute constructor values $scanner = new ReflectionScanner(new ComposerNamespaceResolver()); $scanner->addFilter(new AttributeFilter(EventListener::class)); $result = $scanner->scan(['src/Listener']); foreach ($result as $fqcn => $metadata) { foreach ($metadata->attributes as $attrMeta) { if ($attrMeta->instance instanceof EventListener) { $dispatcher->listen( event : $attrMeta->instance->event, listener: $fqcn, priority: $attrMeta->instance->priority, ); } } }
Plugin System — discover extensions by interface
use KaririCode\ClassDiscovery\Filter\InterfaceFilter; $scanner = new FileScanner(new ComposerNamespaceResolver()); $scanner->addFilter(new InterfaceFilter(PaymentGatewayInterface::class)); $result = $scanner->scan(['plugins/']); foreach ($result as $fqcn => $metadata) { $registry->register(new $fqcn()); }
Refactoring Guard — detect circular dependencies
use KaririCode\ClassDiscovery\Analyzer\{CircularDetector, DependencyAnalyzer}; $scanner = new FileScanner(new ComposerNamespaceResolver()); $result = $scanner->scan(['src/']); $detector = new CircularDetector( new DependencyAnalyzer(), throwOnDetection: true, // throws DiscoveryException::circularDependency() ); $cycles = $detector->check($result); // [['App\A', 'App\B', 'App\C', 'App\A']]
Console — auto-register commands from #[Command]
use KaririCode\ClassDiscovery\Filter\{AttributeFilter, NamespaceFilter}; $scanner = new FileScanner(new ComposerNamespaceResolver()); $scanner->addFilter(new AttributeFilter(Command::class)); $scanner->addFilter(new NamespaceFilter('App\\Console\\Command')); $result = $scanner->scan(['src/Console']); foreach ($result as $fqcn => $metadata) { $application->add(new $fqcn()); }
Scanner Comparison
| Scanner | Parser | Loads Classes | Performance* | Best For |
|---|---|---|---|---|
FileScanner |
token_get_all |
❌ Never | 30–80ms cold / <3ms warm | General listing, attribute names |
AttributeScanner |
FileScanner + filter | ❌ Never | 50–100ms cold / <5ms warm | Attribute-driven discovery |
DirectoryScanner |
FileScanner + constraints | ❌ Never | dep. on I/O | Depth/pattern-bounded scan |
ReflectionScanner |
ReflectionClass |
✅ Required | 300–800ms | Full attribute argument values |
*per 1,000 classes — with cache, warm scans are typically 5–10× faster
Filters
use KaririCode\ClassDiscovery\Filter\{ AttributeFilter, InterfaceFilter, NamespaceFilter, StructuralFilter, CompositeFilter, }; // By attribute (OR — multiple attribute classes) $scanner->addFilter(new AttributeFilter(Route::class, Command::class)); // By implemented interface $scanner->addFilter(new InterfaceFilter(HandlerInterface::class)); // By namespace prefix $scanner->addFilter(new NamespaceFilter('App\\Handler')); // By structural characteristics $scanner->addFilter(new StructuralFilter(isFinal: true, isReadonly: true)); // OR-logic composite (attribute OR interface) $scanner->addFilter(new CompositeFilter( requireAll: false, new AttributeFilter(Route::class), new InterfaceFilter(ControllerInterface::class), ));
Caching
use KaririCode\ClassDiscovery\Cache\{ ChainCacheStrategy, // L1 → L2 chain with automatic promotion FileCacheStrategy, // Atomic writes (temp → rename), OPcache-friendly MemoryCacheStrategy, // In-process, process-lifetime }; // Multi-tier: Memory (L1) → File (L2) $cache = new ChainCacheStrategy( new MemoryCacheStrategy(defaultTtl: 60), new FileCacheStrategy('/var/cache/discovery', defaultTtl: 3600), ); $scanner->setCacheStrategy($cache); // First request : hits filesystem (~60ms) // Every request after: hits memory (<1ms) $result = $scanner->scan(['src/']);
PSR-11 Container Registration
use KaririCode\ClassDiscovery\Integration\PSR11Integration; use KaririCode\ClassDiscovery\Contract\{Scanner, AttributeScanner}; $container->singleton( Scanner::class, fn () => PSR11Integration::createDefaultScanner($cache), ); $container->singleton( AttributeScanner::class, fn () => PSR11Integration::createAttributeScanner($cache), );
Error Handling
use KaririCode\ClassDiscovery\Exception\DiscoveryException; try { $result = $scanner->scan(['/path/that/does/not/exist']); } catch (DiscoveryException $e) { match ($e->getCode()) { 1001 => handlePathNotFound($e), 1002 => handlePathTraversal($e), 1003 => handleSymlinkEscape($e), 1004 => handleMaxDepthExceeded($e), 1005 => handleMaxFilesExceeded($e), 1010 => handleCircularDependency($e), default => throw $e, }; }
CI Integration
GitHub Actions — unified pipeline
name: Quality on: [push, pull_request] jobs: quality: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.4' coverage: pcov - run: composer install --no-progress --no-scripts - run: vendor/bin/kcode init - run: vendor/bin/kcode quality
GitHub Actions — parallel jobs
jobs: cs-check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: { php-version: '8.4' } - run: composer install --no-progress --no-scripts - run: vendor/bin/kcode init && vendor/bin/kcode cs:fix --check analyse: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: { php-version: '8.4' } - run: composer install --no-progress --no-scripts - run: vendor/bin/kcode init && vendor/bin/kcode analyse test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: { php-version: '8.4', coverage: pcov } - run: composer install --no-progress --no-scripts - run: vendor/bin/kcode init && vendor/bin/kcode test
Architecture
Component layout
src/
├── Contract/ 7 interfaces (ISP-compliant)
├── Scanner/ 5 implementations (composition pattern)
├── Result/ 5 final readonly DPOs (ARFA 1.3 P1)
├── Filter/ 5 composable predicates (AND/OR)
├── Cache/ 3 cache strategies (Chain, Memory, File)
├── Analyzer/ 2 analyzers (dependency graph + DFS cycle detection)
├── Integration/ 3 bridges (PSR-16, PSR-11, Configurator)
├── Enum/ 2 PHP 8.1 enums (backed + pure)
└── Exception/ 1 exception class, 10 named constructors
Key design decisions
| Decision | Rationale | ADR |
|---|---|---|
| Token-based parsing | Never loads or executes discovered classes | ADR-001 |
| Zero external dependencies | Works in any PHP 8.4 project, no version conflicts | ADR-002 |
| Immutable result objects | Thread-safe, cacheable, ARFA 1.3 compliant | ADR-003 |
| Composition over inheritance | Scanners compose FileScanner; no deep hierarchies |
ADR-005 |
| Multi-tier cache strategy | Memory (L1) → File (L2), 5–10× warm speedup | ADR-006 |
Specifications
| Spec | Covers |
|---|---|
| SPEC-001 | Component architecture, contracts, scanner strategies |
Project Stats
| Metric | Value |
|---|---|
| PHP source files | 33 |
| Total source lines | ~2,800 |
| External runtime dependencies | 0 |
| Filter types | 5 (Attribute, Interface, Namespace, Structural, Composite) |
| Scanner strategies | 4 (File, Attribute, Directory, Reflection) |
| Cache strategies | 3 (Memory, File, Chain) |
| PHPStan level | 9 (0 errors) |
| Psalm level | 3 (0 errors) |
| Test suite | 222 tests · 440 assertions |
| Line coverage | 95.44% |
| PHP version | 8.4+ |
| ARFA compliance | 1.3 |
KaririCode Ecosystem Integration
| Component | Attributes Discovered |
|---|---|
kariricode/router |
#[Route], #[Middleware], #[Guard] |
kariricode/console |
#[Command], #[Argument], #[Option] |
kariricode/dependency-injection |
#[Singleton], #[Scoped], #[Tagged] |
kariricode/event-dispatcher |
#[EventListener], #[EventSubscriber] |
kariricode/websocket |
#[WebSocketRoute], #[OnConnect], #[OnMessage] |
kariricode/i18n |
#[TranslatableResource] |
Contributing
git clone https://github.com/kariricode/class-discovery.git cd class-discovery composer install vendor/bin/kcode init vendor/bin/kcode quality # Must pass before opening a PR
CI enforces code quality (PHPStan level 9, Psalm, CS-Fixer, PHPUnit) on every push and PR.
License
Part of the KaririCode Framework ecosystem.