PSR-4 и автозагрузка в PHP: рабочие приёмы из практики
Class not found на проде, а локально всё работает. Разбираем, как PSR-4 маппит неймспейсы на файлы, почему регистр имени класса решает всё и когда classmap быстрее автозагрузки.

Классическая картина: код собран локально, тесты зелёные, деплой на боевой Linux-сервер — и Fatal error: Uncaught Error: Class "App\Service\OrderProcessor" not found. Причина в 90% случаев одна: файл называется orderprocessor.php или OrderProcessor.PHP, а класс — OrderProcessor. На macOS и Windows файловая система не различает регистр, на проде — различает. PSR-4 требует точного совпадения имени класса и имени файла, и Composer на это не жалуется при установке.
Разберём, как автозагрузка по PSR-4 работает в связке с Composer 2.x и PHP 8.x, где она молча ломается и когда её стоит менять на classmap. Про PSR-12 и стиль кода — только в конце и одной строкой: это отдельная тема, здесь речь про загрузку классов.
Что вообще делает PSR-4
PSR-4 — это правило, по которому автозагрузчик превращает полное имя класса в путь к файлу. Никакой магии: берётся префикс неймспейса, сопоставляется с базовым каталогом, остаток неймспейса становится путём внутри него, к имени класса дописывается .php.
Пусть в composer.json задано:
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}Тогда класс App\Service\OrderProcessor ищется по пути src/Service/OrderProcessor.php. Префикс App\ отбрасывается, вместо него подставляется src/, разделители неймспейса \ становятся /. Всё.
Три вещи, которые ломаются чаще всего, вытекают прямо из этого правила:
- Регистр. Имя файла должно совпадать с именем класса символ в символ, включая заглавные буквы.
OrderProcessorиOrderprocessor.php— разные файлы на чувствительной ФС. - Разделитель префикса. Ключ
App\\должен заканчиваться на\. Забыли — и Composer сопоставит его как частичное совпадение, отладка займёт час. - Каталог. Значение
src/— путь относительноcomposer.json. Слэш в конце не обязателен, но лучше держать единообразие во всём файле.
Почему после правки composer.json ничего не меняется
Composer не читает секцию autoload на каждый запрос. Он один раз генерирует статические карты в vendor/composer/ и подключает их через vendor/autoload.php. Добавили новый неймспейс в composer.json — карта осталась старой, класс не находится. Лечится одной командой:
composer dump-autoloadЭта команда пересобирает файлы автозагрузки, не трогая зависимости. Её же запускают в CI после каждого изменения composer.json и в деплой-скрипте.
Приём первый: PSR-4 для разработки, classmap для прода
PSR-4 удобен при разработке: добавили класс — он подхватился без пересборки карты (пока структура каталогов совпадает с неймспейсами). Но на каждый неизвестный класс автозагрузчик делает file_exists — обращение к файловой системе. На проекте с сотнями классов и высоким RPS это заметная нагрузка.
Решение — собрать оптимизированную карту классов на деплое:
composer dump-autoload --optimize --classmap-authoritativeФлаг --optimize (короткая форма -o) обходит все PSR-4 каталоги и строит явную карту «класс → файл». Флаг --classmap-authoritative говорит автозагрузчику: если класса нет в карте — его нет вообще, не проверяй файловую систему. Проверки file_exists исчезают.
PSR-4 на проде без оптимизированной карты — это лишний системный вызов на каждый класс, которого ещё нет в кеше. На деплое это исправляется одной командой, а забывают её почти все.
Цена authoritative-режима
Есть подвох. --classmap-authoritative ломает динамически создаваемые классы: если код генерирует классы на лету (прокси Doctrine, моки в тестах, кеш-прокси Symfony) и они не попали в карту на момент сборки — автозагрузчик их не найдёт. Для боевого кода без генерации на лету режим безопасен. Для сборки, где классы догенерируются в рантайме, берите --optimize без --classmap-authoritative: карта есть, но fallback на PSR-4 сохраняется.
Приём второй: несколько путей на один неймспейс
PSR-4 разрешает список каталогов для одного префикса. Composer проверяет их по порядку:
{
"autoload": {
"psr-4": {
"App\\": ["src/", "src-legacy/"]
}
}
}Класс App\Foo сначала ищется в src/Foo.php, потом в src-legacy/Foo.php. Удобно при постепенном переезде старого кода: новый пишете в src/, старый лежит в src-legacy/ под тем же неймспейсом, и вам не приходится трогать все use разом.
autoload-dev — чтобы тестовые классы не текли в прод
Классы тестов не нужны в боевой сборке. Их держат в отдельной секции:
{
"autoload": {
"psr-4": { "App\\": "src/" }
},
"autoload-dev": {
"psr-4": { "App\\Tests\\": "tests/" }
}
}Секция autoload-dev не попадёт в автозагрузку при установке с флагом --no-dev — именно так собирают релиз. Если тестовый класс случайно оказался нужен в рантайме прода, вы это увидите сразу: он не найдётся на сборке без dev-зависимостей.
Приём третий: files для функций, а не классов
PSR-4 грузит только классы, интерфейсы, трейты и энумы — по требованию, когда они впервые упомянуты. Глобальные функции и константы так не подтягиваются: у функции нет момента «первого обращения», который автозагрузчик мог бы перехватить. Для них есть секция files:
{
"autoload": {
"psr-4": { "App\\": "src/" },
"files": ["src/helpers.php"]
}
}Файлы из files подключаются через require на каждый запрос, при подключении vendor/autoload.php. Отсюда правило: держите в них только объявления функций и констант, без тяжёлой логики на верхнем уровне — она выполнится в каждом процессе.
Что выбрать: сравнение режимов dump-autoload
| Режим | Проверки ФС в рантайме | Динамические классы | Когда применять |
|---|---|---|---|
| PSR-4 без флагов | Есть, на каждый ненайденный класс | Работают | Локальная разработка |
--optimize (-o) | Только для классов вне карты | Работают через fallback | Прод с генерацией классов на лету |
--classmap-authoritative | Нет | Ломаются, если не в карте | Прод без динамической генерации |
Если не находится класс — что проверить
- Регистр имени файла. Сравните имя класса и имя файла посимвольно. На чувствительной ФС проверить можно так:
ls -la src/Service/и глазами сверить сclass OrderProcessor. Это причина номер один при переезде с Windows/macOS на Linux. - Забыли dump-autoload. Добавили неймспейс в
composer.json, но не пересобрали карту. Запуститеcomposer dump-autoload. - Собрали с --classmap-authoritative, а класс генерируется в рантайме. Ошибка
Class not foundтолько на проде, локально всё цело. Пересоберите без этого флага. - Неверный ключ в composer.json. Двойные слэши в JSON: в файле должно быть
"App\\", а не"App\". Один слэш экранирует кавычку и ломает разбор. - Класс в файле, который не подключён через vendor/autoload.php. Убедитесь, что точка входа делает
require __DIR__ . '/vendor/autoload.php';до первого обращения к классам.
Что делать дальше
Добавьте composer dump-autoload --optimize в деплой-скрипт сразу после composer install --no-dev — это снимает большинство проблем с производительностью автозагрузки без риска. В CI поставьте линтер имён файлов или запуск на Linux-раннере, чтобы поймать несовпадение регистра до прода, а не после. Если хотите разобраться, чем PSR-4 отличается от устаревшего PSR-0 и почему последний тащил подчёркивания в путях, — смотрите текст самого стандарта на сайте PHP-FIG. Стиль кода (PSR-12) и логирование (PSR-3) — отдельные стандарты, к автозагрузке отношения не имеют.
Репетитор 1—11 класс и ВУЗ: Математика и физика методом наведения на мысль, помощь с ДЗ.
Спросить за 8 ₽Источники: PSR-4: Autoloader — PHP-FIG, Composer — Autoloading and optimization
Частые вопросы
Локально всё работает, а на сервере Class not found. Почему?
У меня другая версия Composer — что-то поменяется?
Что будет, если я не запущу dump-autoload после правки composer.json?
Почему после --classmap-authoritative на проде отвалились Doctrine-прокси?
Можно ли автозагружать функции через PSR-4?
Насколько --optimize реально ускоряет прод?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.