lorisleiva/lody
Lody loads files or PHP classes from one or more paths as a Laravel LazyCollection. Discover classes via PSR-4 resolution, then filter (e.g., non-abstract, instance of) and iterate to register or process them. Configurable path and classname resolving.
Installation:
composer require lorisleiva/lody
No additional configuration is required for basic usage in Laravel.
First Use Case:
Discover and register all non-abstract Node classes in app/Workflow/Nodes:
use Lorisleiva\Lody\Lody;
Lody::classes('app/Workflow/Nodes')
->isNotAbstract()
->isInstanceOf(Node::class)
->each(fn (string $classname) => $this->register($classname));
Where to Look First:
Usage section for core methods (files(), classes()).isInstanceOf(), hasTrait(), and hasMethod().resolvePathUsing() and resolveClassnameUsing() for customization.plugins/ directory for classes implementing PluginInterface and auto-register them.Lody::classes('plugins')
->isInstanceOf(PluginInterface::class)
->each(fn (string $class) => PluginManager::register($class));
if (config('features.workflows.enabled')) {
Lody::classes('app/Workflow/Nodes')
->isNotAbstract()
->each(fn (string $class) => $this->register($class));
}
register() calls in AppServiceProvider with dynamic discovery.// Instead of:
// $this->app->bind('App\Contracts\PaymentGateway', App\StripeGateway::class);
// Use:
Lody::classes('app/Gateways')
->isInstanceOf(PaymentGateway::class)
->each(fn (string $class) => $this->app->bind(PaymentGateway::class, $class));
generateTestData() method to populate test databases.Lody::classes('tests/DataFactories')
->hasMethod('generateTestData')
->each(fn (string $class) => $class::generateTestData());
QueuedCommand.Lody::classes('app/Console/Commands')
->hasTrait(QueuedCommand::class)
->each(fn (string $class) => $this->commands($class));
Lody::classes('legacy')
->hasMethod('process')
->each(fn (string $class) => $this->wrapLegacyClass($class));
Combine with Laravel Facades:
Use Lody alongside app(), config(), or cache() for environment-aware loading:
if (app()->environment('local')) {
Lody::classes('app/DevTools')
->each(fn (string $class) => $this->registerDevTool($class));
}
Custom Finder Instances:
Pass a Symfony\Component\Finder\Finder for advanced filtering (e.g., exclude tests/):
$finder = Finder::create()
->files()
->in(app_path('Services'))
->exclude('tests');
Lody::classesFromFinder($finder)
->isInstanceOf(ServiceInterface::class)
->each(...);
Caching Results: Cache the lazy collection to avoid repeated filesystem scans:
$classes = cache()->remember('discovered_classes', now()->addHour(), function () {
return Lody::classes('app/Modules')
->isInstanceOf(ModuleInterface::class);
});
Event-Driven Discovery:
Trigger discovery on booted or registered events in service providers:
public function boot(): void
{
Lody::classes('app/EventListeners')
->hasTrait(ShouldQueue::class)
->each(fn (string $class) => event(new ClassDiscovered($class)));
}
Path Resolution Quirks:
/ are treated as absolute. Relative paths are resolved from base_path().Lody::resolvePathUsing() to customize logic:
Lody::resolvePathUsing(fn (string $path) => $path);
Classname Resolution Failures:
resolveClassnameUsing.Lody::setAutoloadPath('custom/autoload_psr4.php');
Performance with Large Directories:
recursive: false or limit depth with a custom Finder:
Lody::classes('app/Modules', recursive: false);
False Positives in classExists:
getClassnames() may return invalid classes (e.g., from vendor/).classExists() to filter:
Lody::files('vendor')->getClassnames()->classExists();
Trait/Method Reflection Overhead:
hasTrait() or hasMethod() use reflection, which can be slow for many classes.$classes = Lody::classes('app/Services')->remember('cached_classes', ...);
Windows Path Handling:
/ vs \) may cause issues on Windows.Lody::resolvePathUsing(fn (string $path) => str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path));
Inspect Raw Files:
Use Lody::files()->each(fn (SplFileInfo $file) => dump($file->getPathname())) to debug paths.
Validate Classnames: Check resolved classnames with:
Lody::classes('app')->each(fn (string $class) => dump(class_exists($class)));
Log Filtered Results: Debug filtering chains:
$collection = Lody::classes('app');
$collection->isInstanceOf(SomeClass::class)->each(fn (string $class) => logger()->debug($class));
Custom Resolvers:
resolvePathUsing or resolveClassnameUsing for project-specific logic (e.g., Docker volumes, symlinked paths).Additional Filters:
ClassnameLazyCollection by adding methods like hasAnnotation() or implementsInterface():
// In a service provider:
ClassnameLazyCollection::macro('hasAnnotation', function (string $annotation) {
return $this->filter(fn (string $class) => /* check annotation */);
});
Lazy Collection Macros:
Lody::classes('app')->queued()->each(...); // Macro for `hasTrait(ShouldQueue::class)`
Integration with Laravel Packages:
Lody in your packages to enable auto-discovery of user-provided classes (e.g., app/Extensions/MyPackage/).Performance Optimizations:
LazyCollection caching or pre-load paths for frequently accessed directories.Autoload Path:
Ensure vendor/composer/autoload_psr4.php is up-to-date after adding new PSR-4 mappings:
composer dump-autoload
Base Path: For non-Laravel use, explicitly set the base path:
Lody::setBasePath(__DIR__);
Hidden Files:
By default, hidden files (e.g., .env) are excluded. Use hidden: true to include them:
Lody::files('app', hidden: true);
How can I help you explore Laravel packages today?