composer dump-autoload is busy, I'll do it myself
A Friday night hotfix blocked by a 3-hour deployment pipeline. How classmap-authoritative autoloading made a missing PHP class invisible, and how to manually patch around it without installing anything on a production host.
It’s Friday evening. 6pm Manila, 9pm Sydney, the weekend already underway for half the team. A backfill command needs to run on production. The diff is written, reviewed, and sitting in the merge queue. It cannot be run yet because it hasn’t shipped.
The problem: it won’t ship for hours. Our deployment pipeline at Freelancer looks roughly like this:
- Merge queue validation: ~1 hour
- Master build (functional tests, API e2e, webapp e2e, multiple platform variants): 1 to 3 hours
- Deployment to hosts: ~30 minutes
That’s potentially 4.5 hours before a simple console command can run. On a Friday. With a real business need on the other end.
The decision: patch it in manually.
What was failing
The command class was new. It had never been deployed. When someone tried to run it directly on the host (after copying the file over), Symfony’s service container refused to boot:
In FileLoader.php line 174:
Expected to find class "Freelancer\Phoenix\Command\Contracts\BackfillEmployerIpContractCommand" in file "/var/www/app/src2/Command/Contracts/BackfillEmployerIpContractCommand.php" while importing services from resource "../src2/Command/", but it was not found! Check the namespace prefix used with the resource in /var/www/app/config/services.yaml.The file was there. The namespace was correct. PHP could parse it fine. But Symfony still couldn’t find the class.
Why
Composer has an --classmap-authoritative flag. When you build with it, Composer generates a complete map of every class in the project at build time, and the autoloader uses that map exclusively. PSR-4 directory scanning is completely disabled. Every class lookup is a single array key lookup in memory, which is fast, but it means any file added outside the normal build is invisible until the classmap is regenerated.
Our build pipeline runs:
composer install \ --classmap-authoritative \ --no-dev \ --no-interaction \ --optimize-autoloader \ --prefer-distThe classmap was generated before this file existed. As far as the autoloader was concerned, the class didn’t exist at all. class_exists() returned false even with the file sitting right there on disk.
This is a deliberate performance tradeoff. On a large codebase with thousands of classes, eliminating filesystem fallbacks meaningfully reduces per-request overhead. The cost is that you can’t add classes at runtime.
The fix
Three steps.
1. Copy the file onto the host
ssh user@prod-host "sudo mkdir -p /var/www/app/src2/Command/Contracts \ && sudo chown www-data:www-data /var/www/app/src2/Command/Contracts"
scp ./src2/Command/Contracts/BackfillEmployerIpContractCommand.php \ user@prod-host:/var/www/app/src2/Command/Contracts/BackfillEmployerIpContractCommand.php2. Manually insert the entry into the classmap
Two vendor files need updating: autoload_classmap.php and autoload_static.php. The entry needs to sit in alphabetical order. In this case, just before the existing Consumers entry:
sudo sed -i "/'Freelancer\\\\\\\\Phoenix\\\\\\\\Command\\\\\\\\Consumers\\\\\\\\ConsumerGroupsCommand'/i\\ \'Freelancer\\\\\\\\Phoenix\\\\\\\\Command\\\\\\\\Contracts\\\\\\\\BackfillEmployerIpContractCommand' \=> \$baseDir . '/src2/Command/Contracts/BackfillEmployerIpContractCommand.php'," \/var/www/app/vendor/composer/autoload_classmap.php
sudo sed -i "/'Freelancer\\\\\\\\Phoenix\\\\\\\\Command\\\\\\\\Consumers\\\\\\\\ConsumerGroupsCommand'/i\\ \'Freelancer\\\\\\\\Phoenix\\\\\\\\Command\\\\\\\\Contracts\\\\\\\\BackfillEmployerIpContractCommand' \=> __DIR__ . '/../..' . '/src2/Command/Contracts/BackfillEmployerIpContractCommand.php'," \/var/www/app/vendor/composer/autoload_static.php3. Clear the Symfony service container cache
sudo -u www-data /var/www/app/bin/console cache:clearThe command ran. Done.
Why not just install Composer and run dump-autoload?
The obvious alternative: copy the Composer binary onto the host and run composer dump-autoload --classmap-authoritative --no-dev. A few reasons that wasn’t the right call here.
I was on macOS. The Composer binary is a PHP archive (PHAR) and should be portable, but my local PHP version differed from the host. Running the wrong Composer version against a production vendor directory felt like a bad idea.
The other option was downloading Composer from the internet directly onto a production host. Installing external tooling on a production server is messy, requires cleanup, and isn’t something I’m comfortable with unless there’s no other path. The sed approach touched exactly two files in a predictable, auditable way.
Risk assessment
What could go wrong:
- Malformed classmap entry breaks autoloading entirely, taking down crons and endpoints on that host
- State drift if the manual changes outlast the next deployment
Mitigations:
- Make
.bakcopies of both vendor files before editing. Reverting is a singlemv. - The next deployment regenerates the vendor directory from scratch. Both files get overwritten cleanly. There’s no persistent state to manage.
The risk of leaving things broken overnight outweighed the risk of the patch. It was scoped, reversible, and self-healing.