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

Robot Loader Laravel Package

nette/robot-loader

RobotLoader automatically loads PHP classes, interfaces, and traits from specified directories by scanning files and caching the results. It spares you from manually requiring or registering each class, making it handy for legacy code or small projects without Composer.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require nette/robot-loader

v4.x requires PHP 8.1+. If the app still ships PHP 8.0, pin ^3.4.

Minimal wiring — point it at the one directory that is genuinely dynamic (plugins, generated modules, tenant code), not at app/:

use Nette\Loaders\RobotLoader;

$loader = new RobotLoader;
$loader->addDirectory(base_path('plugins'));                 // absolute path
$loader->setCacheDirectory(storage_path('framework/cache/robot-loader')); // v4.1.3+
// $loader->setTempDirectory(...) still works, deprecated alias
$loader->setAutoRefresh(app()->environment('local'));
$loader->register();                                          // appended to the SPL stack, Composer stays first

Where to look first: getClasses(). It returns ['App\Plugins\Foo\Plugin' => '/abs/path/Plugin.php', ...] — classes, interfaces, traits and enums, mapped from a scan. That array is the whole value proposition; the autoloading side is secondary in a Laravel app.

First real use case — plugin discovery in a service provider:

$this->app->singleton(RobotLoader::class, function () {
    $loader = new RobotLoader;
    $loader->addDirectory(...config('robot-loader.directories'));
    $loader->setCacheDirectory(config('robot-loader.cache'));
    $loader->setAutoRefresh(config('robot-loader.auto_refresh'));
    $loader->register();
    return $loader;
});

// in a booted() callback or a deferred listener:
foreach (app(RobotLoader::class)->getClasses() as $class => $file) {
    $r = new ReflectionClass($class);
    if ($r->isInstantiable() && $r->implementsInterface(Plugin::class)) {
        $this->app->tag([$class], 'plugins');
    }
}

Registering it as a singleton/deferred binding means the scan (and cache read) only happens the first time something actually asks for discovery — no cost on the 90% of requests that never touch plugins.


Implementation Patterns

Discovery-only mode (recommended default). Skip register() entirely and use the loader purely as an index. You get the scan + cache without introducing a second autoload path:

$loader = new RobotLoader;
$loader->addDirectory(base_path('plugins'));
$loader->setCacheDirectory(storage_path('framework/cache/robot-loader'));
$loader->setAutoRefresh(true);

foreach ($loader->getClasses() as $class => $file) {
    require_once $file;              // you control the loading
    // ... inspect, register, build a manifest
}

Config-driven factory, one place to change. Keep directories, excludes, accept-files and cache path in config/robot-loader.php. Because the cache key is derived from that configuration, centralizing it keeps you from accidentally generating two competing cache files.

'directories'  => [base_path('plugins')],
'exclude'      => [base_path('plugins/*/tests'), base_path('plugins/*/vendor')],
'accept_files' => ['*.php'],
'cache'        => storage_path('framework/cache/robot-loader'),
'auto_refresh' => env('APP_ENV') !== 'production',
$loader->excludeDirectory(...config('robot-loader.exclude'));
$loader->setAcceptFiles(...config('robot-loader.accept_files'));

Auto-registering console commands:

if ($this->app->runningInConsole()) {
    $commands = [];
    foreach ($loader->getClasses() as $class => $file) {
        $r = new ReflectionClass($class);
        if (!$r->isAbstract() && $r->isSubclassOf(Illuminate\Console\Command::class)) {
            $commands[] = $class;
        }
    }
    $this->commands($commands);
}

Attribute-driven registration (Laravel 10/11 style, and the cleanest contract for third-party plugins):

foreach ($loader->getClasses() as $class => $file) {
    $r = new ReflectionClass($class);
    if ($r->getAttributes(AsPlugin::class)) {
        $this->app->singleton($class);
    }
}

Deploy warm-up — do not rely on the first request. Add a command and call it from your deploy pipeline after composer install, before traffic shifts:

class RefreshRobotLoader extends Command
{
    protected $signature = 'robot:refresh';

    public function handle(RobotLoader $loader): void
    {
        $loader->rebuild();               // refresh() = rebuild only if stale; rebuild() = always
        $this->info(count($loader->getClasses()) . ' classes indexed');
    }
}

Order matters: composer dump-autoload → robot:refresh → octane:reload / queue:restart.

Manifest generation for admin UIs. Discovery runs in a queued job or artisan command, writes JSON to the cache or DB; the web request reads the manifest. This keeps the tokenizer entirely out of the request path — the best fit for "list available integrations" screens.

Parse-error tolerance for user-supplied code. If plugins are uploaded by customers, wrap discovery in a job and:

$loader->reportParseErrors(false);   // returns silently instead of throwing ParseError

…then validate each discovered file independently so one bad plugin doesn't block the whole registry.


Gotchas and Tips

It is not a Composer replacement — and it isn't trying to be. Composer's autoloader is registered first (from vendor/autoload.php), RobotLoader is appended. For any class Composer already knows, RobotLoader never runs. Don't restructure first-party code around it; scope it to directories Composer can't see at build time.

Duplicate FQCNs fail silently. If a plugin class collides with a Composer-mapped class, Composer wins and you get the other implementation with no error. Enforce a distinct namespace prefix per plugin and assert it at registration time.

The cache directory must be absolute. Since v4.0.2 a relative path throws. storage_path() and base_path() are fine; a bare 'cache' is not.

setTempDirectory() is deprecated in favour of setCacheDirectory() (v4.1.3). Both work; pick one and stay consistent so you don't end up with two cache locations.

Auto-refresh is a per-request stat cost. Leave it on in local/testing so file edits are picked up immediately; turn it off in production and rely on the deploy-time rebuild(). If you disable it and skip the warm-up, the first request after a deploy pays the full scan — and on a cold cache under load you get a thundering herd all trying to build simultaneously. The cache write is atomic (with an internal lock and a Windows retry since 4.1.3), so nobody corrupts it, but latency spikes.

Long-running workers hold a stale map. Octane workers, Horizon/queue workers and artisan serve keep the RobotLoader singleton and its class map in memory across requests. After a deploy, plugin changes are invisible until octane:reload / queue:restart. This is the single most common "why isn't my plugin showing up" bug.

Class lookups are case-sensitive since v3.0.0. getClasses() keys are exact. A mis-cased reference may resolve on a macOS/Windows dev box and fail on Linux. Add a CI test that asserts a specific plugin appears in getClasses() — it catches both case bugs and cache regressions before production.

getClasses() returns everything: interfaces, traits, enums, abstract classes, and #[Attribute] classes. Always filter with ReflectionClass::isInstantiable(), isSubclassOf() or implementsInterface() before binding anything to the container.

Parse errors are thrown by default (since v3.1.0). A single syntax error in a scanned file breaks boot. Fine in development, dangerous in a plugin-upload flow — reportParseErrors(false) plus a separate validation pass.

Changing config invalidates the cache key, generating a brand-new cache file and leaving the old one behind. The key is built from directories/excludes/accept-files via xxHash. Prune the cache directory as part of your deploy, or accept slow growth; at minimum don't be surprised by several files in there.

Don't add vendor/ or node_modules/ to the scan. Even where the defaults exclude them, explicitly re-adding a broad root balloons scan time, cache size, and per-request memory (the whole map is loaded at once). Scope as tightly as possible.

Symlinks are resolved with realpath(). A plugin directory symlinked from a local path resolves to its target, so the cache key and stored paths differ between a symlinked dev setup and a real deploy. Test the deploy layout, not just local.

PHARs are a dead end. getRealPath() can return false inside a PHAR; don't point the scan there.

Files with no named class are remembered, not re-scanned (v3.4.0). They simply won't appear in getClasses() — don't treat a missing entry as "file not parsed."

The //netteloader comment marker was removed in v3.2.1. Legacy docs and StackOverflow answers still reference it; ignore them.

Prefer wrapping over extending. Treat RobotLoader as a sealed component — build a small PluginRegistry around it rather than subclassing. That gives you a place to cache the filtered result, swap in a fake for tests, and add logging when discovery fails.

Testing: give each test run its own cache directory (e.g. storage_path('framework/testing/robot-loader')) and delete it in setUp(), or set setAutoRefresh(true) everywhere. Sharing a cache dir between test cases produces order-dependent failures that look like discovery bugs.

License: the repository metadata reports NOASSERTION; the nette ecosystem is BSD-3-Clause in practice. Get legal confirmation before distributing it inside a commercial package — a checklist item, not a blocker.

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.
muka/shape-reader
tdt/json
refinery29/php-cs-fixer-config
hotmeteor/eco-env
mvaliolahi/hydrate
wearede/tbcpay-php
parsnick/steak
thibaud-dauce/mattermost-php
multilinguals/apollo-client
antaresbr/collection
kr0lik/phpstan-rules
loco/loco
starfederation/datastar-php
yupmin/modern-phystrix
stephenjude/extended-artisan-commands
lumphp/laravel-macroable
lumphp/laravel-collections
lumphp/laravel-contracts
sermepa/sermepa
arkecosystem/fortify