Прапорці -o і -a копіюють із чужих Dockerfile і рідко перевіряють, що вони дають саме тут. Далі механіка на Composer 2.8 і PHP 8.4: що лежить у vendor/composer/, у якому порядку ClassLoader ухвалює рішення, і де кожна з трьох оптимізацій реально економить, а де нічого не змінює.
Що робить ClassLoader на кожен клас
vendor/autoload.php віддає екземпляр Composer\Autoload\ClassLoader, зареєстрований через spl_autoload_register. Дані для нього згенеровані заздалегідь і лежать поруч: autoload_psr4.php (префікс → масив тек), autoload_namespaces.php (PSR-0), autoload_classmap.php (клас → файл), autoload_files.php (те, що підключається безумовно), і autoload_static.php, куди все це складено статичними властивостями одного класу, щоб OPcache тримав готові масиви без повторного парсингу.
Порядок у findFile() фіксований і важливий для всього далі:
isset($this->classMap[$class])- якщо є, повертається шлях, і на цьому все;- якщо classmap оголошений authoritative або клас уже раз не знайшовся у цьому процесі (
missingClasses) - повертаєтьсяfalse; - якщо налаштований APCu-префікс -
apcu_fetch(); findFileWithExtension(): PSR-4, потім PSR-0, потім fallback-теки;- результат (навіть негативний) кладеться в APCu, промах запамʼятовується в
missingClasses.
Крок 4 і є тим, за що платять. Для App\Hiring\Domain\Vacancy будується логічний шлях App/Hiring/Domain/Vacancy.php, далі береться індекс prefixLengthsPsr4 за першим символом імені класу і перебираються префікси від найдовшого до найкоротшого. На кожен префікс, який підходить, і на кожну теку в ньому виконується file_exists(). Тобто кількість звернень до диска дорівнює кількості кандидатів, а не кількості всіх префіксів у проєкті. Один акуратний PSR-4 префікс на теку дає одну перевірку. Проєкт із трьома префіксами, що починаються на однакову літеру, і кількома теками в кожному - вже пʼять-шість.
Промахи дорожчі за влучання. class_exists('Redis'), перевірки на наявність опційного пакета, interface_exists() у фабриках: кожен такий виклик проходить увесь ланцюжок PSR-4 і PSR-0, не знаходить нічого і повертається з false. У межах одного процесу другий такий виклик уже безкоштовний завдяки missingClasses, але в PHP-FPM «один процес» це один запит.
-o: classmap замість пошуку по файловій системі
composer dump-autoload --optimize (або composer install --optimize-autoloader) сканує всі теки з PSR-4 і PSR-0 правил, вашi й вендорські, дістає з кожного файлу оголошені класи і записує їх у autoload_classmap.php. Після цього крок 1 у findFile() закриває майже все: один isset по масиву, який уже в памʼяті процесу.
Про саме сканування:
- Класи, розташовані не там, де вимагає PSR-4, у classmap не потрапляють, і Composer пише про це попередження
does not comply with psr-4 autoloading standard, skipping. Через PSR-4 вони б і так не завантажились, тож classmap тут нічого не ламає, лише робить проблему видимою.composer dump-autoload --optimize --strict-psrперетворює це попередження на помилку з ненульовим кодом виходу, що нормально ставиться в CI. --strict-ambiguousзавалює dump, якщо один клас знайдено у двох файлах. Такий проєкт працює як лотерея залежно від порядку сканування.- Теки з фікстурами, стабами й недосяжним легасі варто прибрати зі сканування:
"autoload": {
"psr-4": { "App\\": "src/" },
"exclude-from-classmap": ["src/Legacy/stubs/"]
}
-o не змінює семантику: PSR-4 fallback на місці, класи, згенеровані після dump, знаходяться як раніше. Платите розміром autoload_static.php і часом самого dump. Для локальної розробки цей прапорець зайвий, бо кожен новий клас вимагатиме перегенерації або тихо піде через fallback.
-a: жодного fallback, і чим це ламає шаровий Dockerfile
composer dump-autoload --classmap-authoritative вмикає -o і додатково ставить $loader->setClassMapAuthoritative(true). Тепер крок 2 обриває пошук: якщо класу немає в classmap, findFile() повертає false, не торкаючись диска взагалі. Жодного file_exists, жодного PSR-4 перебору. Найдешевший можливий варіант, і водночас єдиний, який має шанс зламати робочий застосунок.
Межі поломки варто окреслити точно: loadClass() просто нічого не робить, коли findFile() віддав false, тож spl_autoload_register продовжує ланцюжок. Класи, які реєструють власний автозавантажувач (проксі Doctrine, дублери в тестах, будь-яка кодогенерація з власним loader або через eval), від authoritative не страждають. Страждає рівно одне: те, що покладалося на PSR-4 fallback самого Composer.
Найчастіший спосіб на це наступити - канонічний шаровий Docker-білд:
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --classmap-authoritative
COPY . .
Кеш шарів працює, образ збирається, застосунок падає з Class "App\Http\Kernel" not found. Під час composer install теки src/ ще не існувало, у classmap потрапив лише vendor/, а fallback вимкнений. З -o замість -a така збірка працює, просто без жодної вигоди для власного коду. Правильний варіант розносить установку залежностей і генерацію автозавантажувача:
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader
COPY . .
RUN composer dump-autoload --no-dev --optimize --classmap-authoritative \
&& composer run-script post-autoload-dump
Друга пастка того ж роду: --no-dev викидає з classmap усе, що оголошене в autoload-dev. Якщо продакшен-код хоч десь торкається класу з тестової теки (сідер, що тягне фабрику, консольна команда з хелпера тестів), з fallback він мовчки знайдеться в dev-контейнері й не знайдеться в продакшені. Authoritative просто робить цю різницю детермінованою.
Третя - код, який пише .php файли з класами у PSR-4 теку вже після деплою. Тут або власний автозавантажувач для цієї теки, або $loader->addClassMap() після генерації, або відмова від authoritative.
Плюс, який часто забувають: з authoritative негативна відповідь стає такою ж дешевою, як позитивна. Застосунок із купою class_exists() у бутстрапі виграє на промахах більше, ніж на влучаннях.
APCu: кеш тільки для промахів classmap
composer install --apcu-autoloader (для dump - composer dump-autoload --apcu) додає в autoload_real.php виклик $loader->setApcuPrefix('...'). Далі findFile() кешує в APCu результат пошуку, включно з false, і наступний процес отримує шлях без жодного file_exists.
Перечитайте порядок кроків вище і подивіться, де саме APCu стоїть: після перевірки classmap і після обриву на authoritative. Звідси все практичне:
- разом із
-aAPCu для автозавантаження не дає нічого, бо до кроку 3 виконання не доходить; - разом із
-oвін допомагає лише тим класам, яких у classmap немає; - без
-oвін допомагає всім, і саме тут його місце: середовища, де classmap не згенерувати (кодогенерація в рантаймі, плагінна система, симлінки на локальні пакети під розробку).
Документація Composer формулює це так само: опція корисна, коли authoritative classmap застосувати не можна.
Префікс за замовчуванням генерується випадковим на кожен dump. Це навмисно: після деплою старі записи в APCu просто нікому не адресовані, і застосунок не читає шляхи попереднього релізу. Якщо ви фіксуєте префікс через --apcu-prefix, скидання APCu на деплої стає вашою відповідальністю.
Для CLI користі майже немає. APCu у CLI живе в межах одного процесу (apc.enable_cli=1 не робить сегмент спільним), тож короткі команди щоразу починають з порожнього кешу. Довгі воркери й так тримають усі класи завантаженими після першого джоба.
Як виміряти
Спершу дізнайтеся масштаб. Скільки записів у classmap:
// tools/autoload-stats.php
$map = require __DIR__.'/../vendor/composer/autoload_classmap.php';
printf("classmap: %d entries\n", count($map));
Потім скільки класів реально завантажується на типовому запиті. Найпростіший надійний спосіб - порахувати їх у самому застосунку, у середині запиту:
// у тимчасовому middleware або в terminate()
$classes = array_filter(
get_declared_classes(),
static fn (string $class): bool => str_starts_with($class, 'App\\')
|| str_starts_with($class, 'Illuminate\\'),
);
logger()->info('autoloaded', ['count' => count($classes)]);
Порівняння варіантів автозавантажувача вимірюється прямо через findFile(), без веб-сервера в схемі:
// tools/findfile-bench.php
/** @var \Composer\Autoload\ClassLoader $loader */
$loader = require __DIR__.'/../vendor/autoload.php';
$classes = array_keys(require __DIR__.'/../vendor/composer/autoload_classmap.php');
$start = hrtime(true);
foreach ($classes as $class) {
$loader->findFile($class);
}
printf("%.2f ms / %d lookups\n", (hrtime(true) - $start) / 1e6, count($classes));
Проганяєте цей скрипт після composer dump-autoload, потім після composer dump-autoload -o, потім після -o -a. Цифри будуть ваші, на вашій файловій системі, з вашою структурою префіксів, і саме тому їм можна вірити. Врахуйте два джерела шуму: realpath-кеш PHP (realpath_cache_size, realpath_cache_ttl) робить повторні звернення в одному процесі дешевими, тож кожен варіант міряйте в свіжому процесі; дискового кешу ОС це теж стосується, тож перший прогін відкидайте.
Кількість системних викликів видно напряму:
strace -f -c -e trace=stat,lstat,statx,newfstatat php tools/findfile-bench.php
З -a рядків про stat для класів не буде взагалі. Різниця між dump-autoload і -o покаже, скільки перевірок існування файлу ви платили на кожному холодному запиті.
Для APCu перевіряйте, що кеш справді використовується і що в ньому лежить очікувана кількість записів: apcu_cache_info(true) дає num_hits, num_misses, num_entries, mem_size. Якщо num_entries близький до розміру classmap, ви ввімкнули APCu без -o і платите памʼяттю за те, що дешевше вирішується classmap. Якщо num_misses зростає нескінченно, у вас або замалий apc.shm_size, або хтось генерує класи з непередбачуваними іменами.
Останнє про очікування. Це фіксована економія на кожному запиті, пропорційна кількості класів і структурі ваших префіксів, у діапазоні одиниць мілісекунд. Вона не лікує повільні запити до БД і не замінює OPcache: класи, підняті через opcache.preload, до автозавантажувача не доходять взагалі, тож міряти має сенс уже з увімкненим preload. І php artisan optimize тут ні до чого: він кешує конфіг, роути, події та вʼюшки, а автозавантажувача не торкається.
Робочий мінімум для продакшену: classmap-authoritative в config розділі composer.json, щоб прапорець не залежав від того, хто зібрав образ, і --strict-psr у CI, щоб dump падав до деплою, а не після.