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 a workflow directory:
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 and Configuration sections for quick integration.Lody facade: Core entry point for files() and classes() methods.ClassnameLazyCollection: Methods like isInstanceOf(), hasTrait(), and hasMethod() for filtering.Dynamic Plugin/Extension Registration:
// Auto-register all classes implementing `PluginContract` in `app/Plugins/`
Lody::classes('app/Plugins')
->isInstanceOf(PluginContract::class)
->each(fn (string $class) => PluginManager::register($class));
Conditional Loading with Feature Flags:
// Load classes only if a feature is enabled
Lody::classes('app/Features/Advanced')
->when(fn () => config('features.advanced.enabled'))
->each(fn (string $class) => $this->boot($class));
Service Provider Optimization:
// Replace manual `register()` calls with dynamic discovery
public function register()
{
Lody::classes('app/Providers/EventListeners')
->isInstanceOf(EventListener::class)
->each([$this, 'registerListener']);
}
Test Data Generation:
// Discover and mock classes with `generateTestData()`
Lody::classes('app/Models')
->hasMethod('generateTestData')
->each(fn (string $model) => $this->mock($model));
CLI Command Auto-Discovery:
// Register all commands implementing `QueuedCommand`
Lody::classes('app/Console/Commands')
->hasTrait(QueuedCommand::class)
->each(fn (string $command) => $this->commands($command));
Combine with Laravel Services:
Use Lody in AppServiceProvider or EventServiceProvider to replace hardcoded registrations.
// app/Providers/AppServiceProvider.php
public function boot()
{
Lody::classes('app/Rules')
->isInstanceOf(Rule::class)
->each(fn (string $rule) => Rule::extend(class_basename($rule), $rule));
}
Custom Finder Logic:
Pass a Symfony\Component\Finder\Finder instance for advanced filtering:
use Symfony\Component\Finder\Finder;
$finder = Finder::create()
->files()
->in(app_path('Workflow/Nodes'))
->name('*.php')
->depth(2);
Lody::classesFromFinder($finder)
->isInstanceOf(Node::class)
->each(fn (string $class) => $this->registerNode($class));
Leverage Lazy Collections: Chain methods for complex filtering without loading all classes into memory:
Lody::classes('app/Repositories')
->isInstanceOf(Repository::class)
->doesNotHaveTrait(Cacheable::class)
->each(fn (string $repo) => $this->bind($repo));
Non-Laravel Environments: Set a custom base path for standalone PHP projects:
Lody::setBasePath(__DIR__);
Path Resolution Quirks:
base_path(). Use absolute paths (e.g., '/app/Plugins') for consistency..env) are excluded by default. Use hidden: true to include them:
Lody::files('app/Config', hidden: true);
Classname Resolution Edge Cases:
composer.json autoload paths differ from Laravel’s conventions, override resolveClassnameUsing:
Lody::resolveClassnameUsing(function (SplFileInfo $file) {
return str_replace(
['/', '.php'],
['\\', ''],
$file->getRelativePathname()
);
});
vendor/composer/autoload_psr4.php by default. To exclude vendor classes, filter manually:
Lody::classes('vendor')
->reject(fn (string $class) => str_starts_with($class, 'Vendor\\'));
Performance with Large Directories:
each() process items one by one. For memory-intensive operations, use ->toArray() sparingly.Lody::classes('app/Commands', recursive: false);
Reflection Limitations:
eval() or dynamic bytecode may not be detected. Stick to filesystem-based discovery.hasTrait()) may fail for abstract methods or private/protected members. Use isPublic() or isProtected() filters if needed.Laravel-Specific Assumptions:
app() Helper: The default resolveClassnameUsing relies on Laravel’s app() helper. For non-Laravel use, provide a custom resolver or set setBasePath().Inspect Raw Files/Classes:
// Log all discovered files (for debugging)
Lody::files('app/Plugins')->each(fn (SplFileInfo $file) => Log::debug($file->getPathname()));
// Log classnames before filtering
Lody::classes('app/Nodes')->getClassnames()->each(fn (string $class) => Log::debug($class));
Validate Paths:
Ensure paths are correct by checking base_path():
dd(base_path('app/Workflow/Nodes')); // Verify the path exists
Override Resolvers Temporarily: Test custom logic in a service provider:
Lody::resolveClassnameUsing(function (SplFileInfo $file) {
return 'Custom\\Namespace\\' . str_replace(['/', '.php'], ['\\', ''], $file->getRelativePathname());
});
Handle Missing Classes Gracefully:
Use classExists() to filter invalid classes:
Lody::files('app/Models')->getClassnames()->classExists()->each(...);
Custom Filters:
Extend ClassnameLazyCollection by adding methods to app/Providers/LodyServiceProvider.php:
use Lorisleiva\Lody\ClassnameLazyCollection;
ClassnameLazyCollection::macro('hasAnnotation', function (string $annotation) {
return $this->filter(fn (string $class) => class_has_attribute($class, $annotation));
});
Event-Based Discovery: Trigger events when classes are discovered:
Lody::classes('app/Listeners')
->each(fn (string $listener) => event(new ClassDiscovered($listener)));
Caching: Cache results for performance-critical paths (e.g., plugin discovery):
$plugins = Cache::remember('lody.plugins', now()->addHours(1), function () {
return Lody::classes('app/Plugins')->isInstanceOf(PluginContract::class)->toArray();
});
Windows Path Handling: Normalize paths for cross-platform compatibility:
Lody::resolvePathUsing(function (string $path) {
return Str::replaceFirst(['\\', ':'], '/', $path);
});
Integration with Laravel Packages:
Use Lody in package development to auto-register components:
// In your package's service provider
Lody::classes('vendor/package/src/Commands')
->each(fn (string $command) => $this->commands($command));
How can I help you explore Laravel packages today?