Back to notes
engineering 8 May 2026 4 min read

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.

php symfony composer production debugging

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:

Terminal window
composer install \
--classmap-authoritative \
--no-dev \
--no-interaction \
--optimize-autoloader \
--prefer-dist

The 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

Terminal window
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.php

2. 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:

Terminal window
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.php

3. Clear the Symfony service container cache

Terminal window
sudo -u www-data /var/www/app/bin/console cache:clear

The 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 .bak copies of both vendor files before editing. Reverting is a single mv.
  • 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.