mcaskill/composer-exclude-files
Composer plugin that lets you exclude specific files from autoloading. Declare exclusions in composer.json to keep classmaps lean and stop unwanted files autoload entries from loading—handy for test fixtures, stubs, and optional integrations. Simple configuration, no code changes required.
1. Install it as a real dependency, not a dev dependency. The plugin only does work during composer dump-autoload / composer install, and production deploys run with --no-dev, so it must survive that:
composer require mcaskill/composer-exclude-files
2. Allow the plugin (Composer 2.2+). Without this it is silently skipped and your exclusions do nothing:
composer config allow-plugins.mcaskill/composer-exclude-files true
This writes config.allow-plugins into composer.json — commit it so CI/Docker builds get the same behavior.
3. Audit what is being eagerly loaded. This is the first place to look. Composer's files autoload entries are required on every request, before Laravel even boots:
php -r "print_r(require 'vendor/composer/autoload_files.php');"
Cross-reference each entry with composer why <vendor/package>. On a typical Laravel app the usual suspects are symfony/polyfill-* bootstraps, symfony/deprecation-contracts/function.php, ralouphie/getallheaders, paragonie/random_compat, and the odd package shipping global helper functions you never call.
4. Add the exclusion block to your root composer.json (check the README for your pinned version — the key/format is extra.exclude-files with a list of vendor-relative paths/globs):
{
"extra": {
"exclude-files": [
"vendor/symfony/polyfill-php80/bootstrap.php",
"vendor/symfony/polyfill-php81/bootstrap.php"
]
}
}
5. Regenerate and verify:
composer dump-autoload -vvv
php -r "print_r(require 'vendor/composer/autoload_files.php');" # entries should be gone
php artisan about && vendor/bin/phpunit --testsuite=Feature
First use case: killing polyfill bootstraps for PHP versions your app no longer supports. It is a safe, measurable win — a handful of requires and function_exists guards removed from every request — and it validates the whole toolchain (plugin allowed, paths correct, dump-autoload wired into your deploy) before you try anything riskier.
Audit → prune → measure loop. Treat this as a performance/cleanup task, not a config you set once. Script the baseline (autoload_files.php entry count, php -r timing of require vendor/autoload.php, or Blackfire/Telescope request timing), prune a batch, re-measure. Keep a note in the PR of why each file was excluded so the next developer doesn't re-add it.
Group exclusions by package with comments in your PR, not in JSON. composer.json doesn't support comments, so maintain a short docs/autoload-exclusions.md mapping each path → package → reason → date. This is the single most useful habit with this plugin, because exclusions are invisible at runtime.
Exact paths, not package names. The plugin strips entries from the generated autoload_files.php, so match the path as it appears in that file. Use globs only for families you control (e.g., every symfony/polyfill-php8* bootstrap) and prefer exact paths for one-offs so a package update that adds a new file doesn't silently get excluded too.
Resolve duplicate-symbol conflicts. The classic case: two packages ship a global function with the same name, and Composer's files autoloader fatals with "Cannot redeclare". Excluding one package's file is a legitimate fix — but then confirm every caller resolves to the survivor (grep -r "function_name(" app/ vendor/) and add a test that exercises it.
Handle missing extensions. If a package's autoload.files entry hard-requires an extension (or PHP version) your production image lacks, exclude that file and keep using the package's classes. This is often cleaner than forking the package or dropping it entirely.
Keep it out of the Laravel layer. There is no service provider, no config publish, no Artisan command — this is purely a Composer-time concern. Don't try to wrap it; instead, make the deploy pipeline assert it worked (see below). After changing it, php artisan optimize:clear and, on Octane, php artisan octane:reload.
Assert exclusions in CI/deploy. A tiny post-install check stops regressions cold:
composer install --no-dev --optimize-autoloader
php -r '$f = require "vendor/composer/autoload_files.php";
foreach (["vendor/symfony/polyfill-php80/bootstrap.php"] as $p)
if (in_array(realpath($p), array_map("realpath", $f), true)) { fwrite(STDERR, "exclusion missing: $p\n"); exit(1); }'
Docker pattern. Multi-stage builds often run composer install --no-plugins --no-scripts for speed/security. That flag combo defeats this plugin entirely — either allow plugins in the build stage or run the assert step above as a build gate.
Pair with --optimize-autoloader. Classmap optimization does nothing for files entries — they remain eager. This plugin is one of the few levers you have over that specific cost, which is exactly why it earns a slot in your deploy config.
allow-plugins is the #1 silent failure. On Composer 2.2+ an unallowed plugin is skipped without an obvious error in non-interactive mode. Run composer diagnose and look for the plugin line if exclusions "don't apply."--no-plugins (and --no-scripts) silently disables everything. Common in Dockerfiles and CI caches. If you see the files still in autoload_files.php in prod but not locally, this is almost always why.extra without dumping does nothing. composer install normally re-dumps, but --no-autoloader and some cache-restore paths don't. When in doubt: composer dump-autoload -vvv.laravel/framework's src/Illuminate/Foundation/helpers.php and src/Illuminate/Support/helpers.php are eagerly loaded and every Blade/controller call depends on them. Excluding them yields Call to undefined function deep in the framework with a confusing stack trace.autoload_static.php is regenerated too. Never hand-edit either file in vendor/; it's overwritten on every dump and won't exist in a clean CI checkout./ silently matches nothing — dev on macOS may hide this.vendor-dir. If you've relocated vendor, write paths relative to the project root as documented, not relative to vendor/.vendor/autoload.php, so a bad exclusion usually blows up locally before it reaches prod — don't disable the suite to "make it pass."autoload events), so the blast radius if it breaks is small and detectable. Pin it, review it on every Composer major/PHP upgrade, and keep the escape hatch in mind: a 30-line custom Composer plugin on post-autoload-dump, or a script that rewrites autoload_files.php, reproduces this behavior if the package ever stalls.composer dump-autoload -vvv shows plugin activity and event order. If an exclusion doesn't stick, check whether another plugin is rewriting the autoloader afterward — plugin ordering is registration-order dependent.autoload.files. Root-level files you declare yourself in composer.json are handled on a slightly different path: if you put a file there and list it in exclude-files, the intent is ambiguous and the result may surprise you. Remove it from autoload.files instead.How can I help you explore Laravel packages today?