hanneskod/classtools
Scan the filesystem for PHP classes, interfaces, and traits using Symfony Finder. Build a class-to-file map, detect parse/syntax errors, and iterate results as ReflectionClass objects, with optional autoloading for discovered classes.
Installation:
composer require hanneskod/classtools
Basic Usage:
use hanneskod\classtools\Iterator\ClassIterator;
use Symfony\Component\Finder\Finder;
$finder = Finder::create()->in(app_path('Http/Controllers'));
$iterator = new ClassIterator($finder);
// Get class map (class name => SplFileInfo)
$classMap = $iterator->getClassMap();
// Iterate over ReflectionClass objects
$iterator->enableAutoloading();
foreach ($iterator as $reflectionClass) {
echo $reflectionClass->getName() . "\n";
}
First Use Case:
$unusedClasses = $iterator
->not($iterator->where('isUsed')) // Custom filter (see below)
->getClassMap();
TypeFilter, NamespaceFilter).MinimizingWriter).$iterator = new ClassIterator(Finder::create()->in(base_path('app/Providers')));
$iterator->enableAutoloading();
foreach ($iterator->type('Illuminate\Support\ServiceProvider') as $provider) {
$this->app->register($provider->getName());
}
Command classes implementing ShouldQueue.$iterator = new ClassIterator(Finder::create()->in(app_path('Console/Commands')));
$iterator->enableAutoloading();
foreach ($iterator
->type('Illuminate\Console\Command')
->where(fn($class) => in_array('ShouldQueue', class_uses($class->getName())))
as $command) {
// Process queued commands
}
$iterator = new ClassIterator(Finder::create()->in(app_path('DTO')));
$iterator->enableAutoloading();
$mergedCode = $iterator->transform(new \hanneskod\classtools\Transformer\MinimizingWriter);
file_put_contents(public_path('api-docs/dtos.php'), $mergedCode);
app/Http.$iterator = new ClassIterator(Finder::create()->in(app_path('Http')));
$errors = $iterator->getErrors();
if (!empty($errors)) {
throw new \RuntimeException("Syntax errors found: " . implode(', ', $errors));
}
Artisan Commands:
use hanneskod\classtools\Iterator\ClassIterator;
use Symfony\Component\Finder\Finder;
class ClassScannerCommand extends Command
{
protected $signature = 'classes:scan {--path= : Path to scan}';
protected $description = 'Scan classes in a directory';
public function handle()
{
$iterator = new ClassIterator(Finder::create()->in($this->option('path')));
$iterator->enableAutoloading();
foreach ($iterator as $class) {
$this->line($class->getName());
}
}
}
Service Provider Binding:
$this->app->singleton('class.scanner', function () {
return new ClassIterator(Finder::create()->in(app_path('Modules')));
});
Event Listeners:
public function handle()
{
$iterator = app('class.scanner');
$iterator->enableAutoloading();
foreach ($iterator->type('App\Events\ShouldLog') as $event) {
Log::info("Discovered event: {$event->getName()}");
}
}
Custom Filters:
Extend \hanneskod\classtools\Iterator\Filter\AbstractFilter to add Laravel-specific logic:
class UsesQueueFilter extends AbstractFilter
{
public function accept($class)
{
return in_array('ShouldQueue', class_uses($class->getName()));
}
}
Usage:
$iterator->filter(new UsesQueueFilter());
Cache Results:
$cache = new \Illuminate\Filesystem\Filesystem();
$cacheKey = 'classes_scan_' . md5(app_path('Http'));
if ($cache->exists($cacheKey)) {
$classMap = unserialize($cache->get($cacheKey));
} else {
$iterator = new ClassIterator(Finder::create()->in(app_path('Http')));
$classMap = $iterator->getClassMap();
$cache->put($cacheKey, serialize($classMap), now()->addHours(1));
}
Limit Scans to Relevant Paths:
Use Laravel’s app_path(), config_path(), or database_path() to avoid scanning irrelevant directories.
Unit Test Filters:
public function testUsesQueueFilter()
{
$iterator = new ClassIterator(Finder::create()->in(__DIR__.'/stubs'));
$iterator->enableAutoloading();
$filter = new UsesQueueFilter();
$queuedClasses = iterator_to_array($iterator->filter($filter));
$this->assertCount(1, $queuedClasses);
$this->assertEquals('App\Events\QueuedEvent', $queuedClasses[0]->getName());
}
Mock ReflectionClass:
Use ReflectionClass::newInstanceWithoutConstructor() in tests to avoid autoloading.
Autoloading Conflicts:
enableAutoloading() may trigger Laravel’s autoloader recursively, causing memory spikes or infinite loops.spl_autoload_register() with a custom loader or disable Laravel’s autoloader temporarily:
spl_autoload_unregister([$loader, 'load']);
$iterator->enableAutoloading();
// Process classes...
spl_autoload_register([$loader, 'load']);
Namespace Resolution:
class User) may not resolve correctly if the file isn’t in the root directory.Finder includes the correct base path:
Finder::create()->in(base_path())->files()->name('*.php');
Syntax Error Handling:
getErrors() returns file paths, not line numbers or context.Symfony\Component\Finder\SplFileInfo for details:
foreach ($iterator->getErrors() as $file) {
$this->error("Syntax error in {$file} (line: " . (new \SplFileInfo($file))->getLineEnd() . ")");
}
Windows Paths:
\) may break on Unix systems.$realPath = str_replace('\\', '/', $splFileInfo->getRealPath());
ReflectionClass Caching:
ReflectionClass::newInstance() can be slow.ReflectionClass instances:
static $reflectionCache = [];
if (!isset($reflectionCache[$className])) {
$reflectionCache[$className] = new \ReflectionClass($className);
}
Verify Class Map:
foreach ($iterator->getClassMap() as $class => $file) {
$this->info("{$class} => {$file->getRealPath()}");
}
Check Autoloading:
if (!$iterator->isAutoloadingEnabled()) {
$iterator->enableAutoloading();
}
Inspect Filters:
$filtered = $iterator->type('App\Model')->inNamespace('App\\');
foreach ($filtered as $class) {
$this->line($class->getName());
}
How can I help you explore Laravel packages today?