Composer, автозагрузка PSR-4 и пространства имён

Вопрос про инструмент, которым пользуются каждый день, но редко задумываются, как он устроен.

PSR-4 — стандарт автозагрузки, который задаёт соответствие между префиксом пространства имён и каталогом на диске: App\Service\Mailer ищется в src/Service/Mailer.php.

Вопрос 1: «Что происходит, когда вы пишете new App\Service\Mailer(), а файл не подключён?»

Что проверяет интервьюер

Понимаете ли вы, что автозагрузка — не магия Composer, а механизм самого языка. Когда PHP встречает неизвестный класс, он по очереди вызывает функции, зарегистрированные через spl_autoload_register(), передавая им полное имя класса. Задача автозагрузчика — подключить нужный файл; если ни один этого не сделал, будет Error: Class not found.

<?php
// упрощённая версия того, что делает Composer
spl_autoload_register(function (string $class): void {
    $prefix = 'App\\';
    $baseDir = __DIR__ . '/src/';

    if (!str_starts_with($class, $prefix)) {
        return; // не наш префикс — пусть попробует следующий загрузчик
    }

    $relative = substr($class, strlen($prefix));
    $file = $baseDir . str_replace('\\', '/', $relative) . '.php';

    if (is_file($file)) {
        require $file;
    }
});

Composer генерирует такой загрузчик автоматически по секции autoload в composer.json:

{
  "require": {
    "php": ">=8.2",
    "monolog/monolog": "^3.5"
  },
  "require-dev": {
    "phpunit/phpunit": "^11.0"
  },
  "autoload": {
    "psr-4": { "App\\": "src/" },
    "files": ["src/helpers.php"]
  },
  "autoload-dev": {
    "psr-4": { "App\\Tests\\": "tests/" }
  }
}

После этого достаточно одной строки require __DIR__ . '/vendor/autoload.php'; во входном файле — дальше классы подтягиваются сами. На проде обязательно собирают оптимизированную карту классов: composer install --no-dev --optimize-autoloader превращает поиск по файловой системе в готовый массив «класс → путь».

Вопрос 2: «Чем composer.json отличается от composer.lock

composer.jsonчто хочет проект: диапазоны версий (^3.5), скрипты, автозагрузка
composer.lockчто реально установлено: точные версии и хэши всех пакетов, включая транзитивные

Правила, которые ждут в ответе: lock-файл коммитится в репозиторий; на сервере запускается composer install (ставит ровно то, что в lock), а composer update — только осознанно и локально, потому что он двигает версии и переписывает lock. Оператор ^3.5 разрешает обновления до 4.0 (не включая), ~3.5 — только до 3.6 (не включая).

Вопрос 3: «Как работают пространства имён?»

<?php
namespace App\Mail {
    class Mailer {
        public function send(string $to): void {
            echo "письмо для $to отправлено\n";
        }
    }
}

namespace App {
    use App\Mail\Mailer;

    $mailer = new Mailer();
    $mailer->send('user@example.com');
    echo Mailer::class, "\n";
    echo \strlen('abc'), "\n";
    echo __NAMESPACE__, "\n";
}

Вывод:

письмо для user@example.com отправлено
App\Mail\Mailer
3
App

Что здесь стоит проговорить:

  • use — это псевдоним времени компиляции, а не подключение файла. Он лишь говорит «в этом файле Mailer означает App\Mail\Mailer».
  • Имя, начинающееся с обратного слэша (\strlen), — абсолютное. Без слэша PHP сначала ищет App\strlen и только потом, для функций и констант, откатывается к глобальному пространству. Для классов такого отката нет: new Exception() внутри namespace без use вызовет ошибку.
  • Mailer::class возвращает полное имя строкой — удобно и безопасно при рефакторинге.
  • Два блока namespace в одном файле разрешены синтаксически, но в реальном коде так не пишут: PSR-4 требует один класс на файл.

Про PSR в целом

PSR (PHP Standard Recommendation) — рекомендации группы PHP-FIG, благодаря которым библиотеки разных авторов совместимы между собой. Достаточно назвать основные:

PSR-1 / PSR-12стиль кода и оформление файлов
PSR-4автозагрузка классов по namespace
PSR-3интерфейс логгера (LoggerInterface)
PSR-6 / PSR-16кэш: полный и упрощённый интерфейсы
PSR-7 / PSR-15 / PSR-17HTTP-сообщения, middleware и фабрики
PSR-11контейнер внедрения зависимостей

Типичные ошибки кандидатов

  • Думают, что use подключает файл. Файл подключает автозагрузчик, use — только сокращает имя.
  • Не коммитят composer.lock или запускают на проде composer update — и получают неожиданные версии пакетов.
  • Путают require и require-dev, из-за чего PHPUnit уезжает на прод.
  • Не знают про --optimize-autoloader и про то, что PSR-4 чувствителен к регистру: App\service и App\Service — разные вещи на Linux.
  • Кладут в один файл несколько классов и удивляются, что автозагрузка их не находит.

Как ответить кратко

«PHP при встрече с неизвестным классом зовёт функции из spl_autoload_register. Composer по секции autoload.psr-4 генерирует такой загрузчик: префикс namespace отображается в каталог, имя класса — в имя файла. composer.json описывает желаемые диапазоны версий, composer.lock фиксирует точные — его коммитим, на сервере делаем composer install --no-dev --optimize-autoloader. use — это псевдоним на этапе компиляции, а не подключение файла».

Проверьте себя
1. Что делает оператор use в начале файла PHP?
AПодключает файл с классом, как require
BРегистрирует класс в автозагрузчике Composer
CИмпортирует все классы указанного namespace
DСоздаёт псевдоним для полного имени класса на этапе компиляции
2. Какую команду Composer нужно запускать на production-сервере при деплое?
Acomposer install --no-dev --optimize-autoloader
Bcomposer update
Ccomposer require --dev
Dcomposer dump-autoload --dev
3. Внутри namespace App написано new Exception(). Что произойдёт?
APHP найдёт глобальный класс Exception благодаря fallback
BБудет искаться App\Exception и, если его нет, возникнет ошибка
CКод не скомпилируется на этапе парсинга
DБудет создан stdClass