Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Lody Laravel Package

lorisleiva/lody

Lody is a Laravel package that loads PHP files and classes from any path as lazy collections, letting you filter by abstraction or interface and chain methods like each. It resolves classnames via PSR-4 conventions and paths relative to your app's base path.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with a single Composer command — there is no service provider to register, no config file to publish, and no migrations:

composer require lorisleiva/lody

Inside a Laravel app it works on first call because base_path() is used to resolve relative paths. Read the README's References section first; it is the entire API surface in ~40 lines. Everything else in the package is either a Symfony Finder wrapper or a LazyCollection subclass with reflection helpers.

The canonical first use case is replacing a hand-maintained registration array with directory-based discovery. Given a contract:

interface Node { public function handle(array $payload): array; }

and nodes living in app/Workflow/Nodes, drop this into a service provider's boot() method:

use Lorisleiva\Lody\Lody;

Lody::classes('app/Workflow/Nodes')
    ->isNotAbstract()
    ->isInstanceOf(Node::class)
    ->each(fn (string $class) => $this->app->tag([$class], 'workflow.nodes'));

Sanity-check discovery before wiring anything up. ->dump() shows resolved classnames, ->count() gives a number, and ->all() materialises the list:

Lody::classes('app/Workflow/Nodes')->dump();
// ["App\Workflow\Nodes\SendEmail", "App\Workflow\Nodes\ChargeCard", ...]

If dump() is empty, the problem is almost never Lody — it is a path typo, a stale autoload map, or a namespace that doesn't map cleanly to a directory.

Implementation Patterns

Interface-based plugin registries. The highest-value pattern is discovery qualified by a contract. Combine isNotAbstract() with isInstanceOf() so that intermediate base classes don't get registered:

Lody::classes('app/Plugins')
    ->isNotAbstract()
    ->isInstanceOf(Plugin::class)
    ->each(fn (string $class) => $this->app->singleton($class));

Resolve lazily at the point of use (app($class)), never inside the each() callback, so a broken plugin only fails when invoked rather than at boot.

Multi-module monoliths. Pass an array of paths instead of scanning app/ wholesale. Scoping the scan to the feature directories you actually own keeps the file count — and therefore the reflection cost — proportional to the registry:

Lody::classes(['app/Auth/Actions', 'app/Billing/Actions', 'app/Reporting/Actions'])
    ->isInstanceOf(Action::class)
    ->isNotAbstract();

Attribute-driven qualification. Lody has no hasAttribute() helper, so drop to filter() and read the reflection yourself. This is the cleanest way to make discovery opt-in per class:

Lody::classes('app/Handlers')
    ->isNotAbstract()
    ->filter(fn (string $class) => (new ReflectionClass($class))
        ->getAttributes(AsHandler::class) !== [])
    ->each(fn (string $class) => $this->register($class));

Raw files when you don't need classnames. Reach for Lody::files() when the target isn't PHP — Blade views, JSON fixtures, migration stubs, YAML config. You get SplFileInfo objects and can chain standard collection methods:

Lody::files('resources/views/mail')
    ->filter(fn (SplFileInfo $f) => $f->getExtension() === 'blade.php')
    ->map(fn (SplFileInfo $f) => $f->getRelativePathname());

Custom finders for anything the boolean flags don't express. recursive: and hidden: cover the common cases; Finder::create() covers the rest — filename globs, depth limits, size, modification time:

Lody::classesFromFinder(
    Finder::create()->files()->in(app_path('Nodes'))->name('*Node.php')->depth(1)
);

Build a manifest at deploy time. Discovery is filesystem traversal plus reflection per candidate — correct once per deploy or once per worker boot, wrong per request. Cache the resolved classnames in a config or bootstrap cache keyed by a version/hash, then hydrate with LazyCollection::make($manifest). Wire the cache warm step into the deploy pipeline after composer dump-autoload.

Order-of-operations in providers. Do discovery in boot(), not register(). Class filters autoload candidate files, and autoloading a class whose constructor or static initialiser depends on config that isn't loaded yet produces confusing failures.

Gotchas and Tips

Under-discovery is silent. classExists() — invoked implicitly by Lody::classes() — silently drops any name that doesn't resolve to a real class. There is no warning, no log entry, no exception. A plugin that stops being registered simply stops running. When debugging, split the chain to see the raw resolution before the filter:

Lody::files('app/Plugins')->getClassnames()->dump(); // raw, unresolved
Lody::classes('app/Plugins')->dump();                // post-classExists

The difference between those two outputs tells you whether the problem is PSR-4 mapping or the filter chain.

A missing path returns an empty collection (behaviour introduced in v0.7.1). A typo in 'app/Workflow/Node' looks identical to a directory with no matching classes. Assert ->isNotEmpty() in your tests, and add a boot-time guard if the registry must never be empty:

$nodes = Lody::classes('app/Workflow/Nodes')->isNotAbstract()->isInstanceOf(Node::class);
abort_if($nodes->isEmpty() && ! $this->app->isLocal(), 500, 'Node registry empty');

PSR-4 resolution reads vendor/composer/autoload_psr4.php. Add a new namespace to composer.json and discovery won't see it until you run composer dump-autoload. This is the single most common cause of "Lody found nothing" in a working app. If your autoload file lives elsewhere, point at it in a provider with Lody::setAutoloadPath().

Optimised/authoritative classmaps break the heuristic. PSR-4 reverse-resolution assumes one class per file, at a path derived from the namespace. Files containing multiple classes, conditionally declared classes, classmap-only entries, and classes declared outside their namespace's directory all resolve to names that fail classExists() and vanish. If production runs composer install --classmap-authoritative, verify discovery against the production build, not just your local one.

Filters autoload candidate files — never scan untrusted code. isInstanceOf, hasTrait, hasMethod, and classExists all trigger autoloading. One plugin file with a missing optional dependency, a fatal at include time, or an expensive static initialiser can break boot or add hundreds of milliseconds. Scope scans to first-party directories; treat vendor scanning as a deliberate, measured decision.

Iterate once, or materialise. These collections are lazy and backed by a filesystem walk — each pass can re-traverse. If you need to loop over the result twice, call ->all() (or cache it) first rather than chaining two each() calls onto the same instance.

Config resolvers are static and global. resolvePathUsing, resolveClassnameUsing, setAutoloadPath, and setBasePath mutate package-wide state. Set them once, in one provider, ideally guarded by $this->app->runningInConsole(). If two packages both override resolvePathUsing, the last boot wins and the failure is non-obvious.

Path resolution quirks. Relative paths are prefixed with base_path(); anything starting with DIRECTORY_SEPARATOR is left untouched. For absolute paths, prefer explicit base_path('...') or app_path('...') over relying on the leading-slash branch — the check is platform-dependent and easy to get wrong when generating paths at runtime.

Version-coupled to Laravel majors. The release history is a strict ratchet: add the newest Laravel major, drop the oldest (v0.7.0 added Laravel 13, dropped 10). With a single maintainer and 0.x versioning, pin an exact version and plan for a fork if upstream lags your framework upgrade.

Useful extension points. Beyond the resolvers, filesFromFinder / classesFromFinder accept any configured Symfony Finder, and since the collection classes extend LazyCollection, macros registered via LazyCollection::macro() are available inside your chains — a reasonable place to park your own hasAttribute() helper so all registries share one definition.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robinflyhigh/livewire-datatables
rudashi/optima
misakstvanu/laravel-fortify-passkeys
insology/laravel-fatture-in-cloud
roundly-consulting/workflows-for-laravel
ipregistry/ipregistry-php
quellabs/canvas-smarty
quellabs/signal-hub
goodport/laravel-js-error-logger
szabrob/laravel-billingo
devkussema/acapapay-laravel
apartejs/laravel
smronju/nativephp-geolocation
luppakorva/laravel-nist-password-rules
owenmelbz/sashimi
gui1535/sql-to-eloquent
hashtap/laravel-responsecache
pnscripts/laravel-proverka-lite
bhawdeadlydan/laravel-accountkit
jlawrence/eos