В 1999 году Stig Bakken основал PEAR, репозиторий переиспользуемого кода для PHP, и полтора десятилетия это оставался главный способ достать чужую библиотеку. В марте 2012 года вышел первый релиз Composer, и за пару лет в него переехала почти вся экосистема, а PEAR остался в учебниках. Сегодня актуальная ветка Composer 2.10 держит контакт с реестром Packagist, в котором около 469 тысяч пакетов, почти 6 миллионов версий и свыше 206 миллиардов установок по счётчику реестра. За числами скрывается вопрос каждого, кто сопровождает большой проект годами: почему сборка, работавшая вчера, обязана собраться и завтра, и причём здесь манифест и замок. Ниже разбор того, чем PEAR был неудобен, что предложил Composer и как выстроить годы стабильной поддержки.
Реальные причины провала PEAR и боль ручного разрешения зависимостей
PEAR был для своего времени передовым проектом: центральный репозиторий, структурированные пакеты, единый стиль кода. Беда пряталась в механике установки. Пакеты ставились глобально, в системный каталог вроде /usr/share/pear, и одна библиотека обслуживала сразу все сайты машины. Когда проектов два и второму нужна другая версия той же библиотеки, конфликт решается трюками с правкой кода или отдельной машиной.
Каналы подключались вручную, и рабочее место PHP-разработчика тех лет выглядело так:
pear channel-discover pear.phpunit.de
pear install phpunit/PHPUnit
Цепочки зависимостей разрешались головой: в инструкции к пакету писали, какие ещё пакеты поставить и в каком порядке, и порядок этот соблюдался человеком. Воспроизводимой установки не существовало: сегодня сайт собрался с одной версией библиотеки, завтра после обновления системы уже с другой, и никто не зафиксировал разницу. Автозагрузки не было, была конвенция имён: класс Text_Password лежал в файле Text/Password.php, каждый пакет тянул свои require_once и требовал настройки include_path.
Идеи PEAR при этом были верными: переиспользование кода, центральный репозиторий, стандарты оформления. Упёрлось всё в глобальность установки и ручной труд, который не масштабируется. Когда у экосистемы появился инструмент, снявший оба ограничения, выбор она сделала быстро.
Обновление писало поверх старой версии, истории версий не существовало, откат был ручной археологией, а список пакетов хранила машина, а не проект: переезд на новый сервер восстанавливал окружение по памяти администратора.
Механика Composer ограничения версий и файл-замок
Composer держит проект на двух файлах. В composer.json объявляются ограничения, а не версии: каретка в записи ^1.4.2 означает любую версию от 1.4.2 до 2.0.0 не включительно, тильда в ~1.4.2 сужает коридор до 1.5.0, а для веток 0.x каретка ограничивает минором, потому что там минор по сути мажор. Встроенный решатель перебирает граф пакетов и ищет совместимую комбинацию, человеку остаётся объявить желания:
{
"require": {
"php": "^8.3",
"ext-json": "*",
"symfony/console": "^7.2",
"monolog/monolog": "^3.7"
}
}
Новые пакеты добавляются одной командой, старые поясняют себя по запросу:
composer require monolog/monolog
composer require --dev phpunit/phpunit:^10
composer show --tree
Флаг --dev кладёт пакет в секцию require-dev, которая не попадает на боевые серверы при install --no-dev. Команда show --tree рисует дерево резолва, и в крупном проекте это дерево легко дорастает до сотни с лишним пакетов, которых никто не ставил руками.
Второй файл, composer.lock, фиксирует найденное: точные версии, контрольные суммы, хеш манифеста. Команда install читает замок и ставит байт в байт то же, что стояло у коллеги вчера и на стенде позавчера. Команда update пересчитывает ограничения и переписывает замок, её вызывают осознанно, а не на всякий случай:
composer install --no-dev --optimize-autoloader
composer update --with-all-dependencies
Путать эти команды не стоит: после правки composer.json установщик напомнит, что замок не соответствует манифесту, и осознанный пересчёт выполняется командой update. Когда дерево растёт, полезно спрашивать его у самого установщика:
composer why guzzlehttp/psr7
composer validate
Первая команда показывает, кто тянет пакет в дерево и какими ограничениями, вторая проверяет синтаксис манифеста перед коммитом. Конфликт версий решатель тоже объясняет словами: он печатает пересекающиеся требования, и лечится это обычно расширением коридора или фиксацией транзитивной версии в корневом require.
Режим стабильности по умолчанию ограничивает решателя стабильными релизами, пометка в манифесте открывает ветки разработки для тестирования будущего. Точная фиксация вида 7.2.0 без каретки заявляет, что обновление будет ручным: так закрепляют пару критичных пакетов, каретками живут остальные. Такая дисциплина и есть фундамент многолетней поддержки.
Packagist в цифрах и параллельные загрузки второй версии
Composer 2.10.3, патч от 27 августа 2026 года, работает с этим объёмом без пота, и скорость досталась от версии 2.0, вышедшей 24 октября 2020 года: тогда переписали протокол обмена метаданными с реестром и научили установщик качать архивы параллельно, что на крупных проектах экономит минуты на каждом развёртывании.
Параллельность съела самое узкое место установки, ожидание сети: загрузки идут пачкой, и минуты сжались до секунд даже на дереве в сотню пакетов. Пакеты ставятся из архивов dist, а при необходимости из исходников source, и это избавляет боевые серверы от git-клиентов. Реестр хранит метаданные, архивы качаются напрямую с хостингов пакетов по адресам из манифестов. Все версии остаются доступны в реестре годами, поэтому старый замок продолжает находить свои архивы и через пять лет. Кэш Composer хранит скачанное локально, повторные установки не ходят в сеть вообще.
Каждая запись замка несёт контрольную сумму архива, и установщик сверяет байты при загрузке: подменённый архив падает на контрольной сумме, не добравшись до каталога vendor. Сверка работает и при установках из кэша, поэтому подмена посреди жизни проекта не пройдёт тихо. Автозагрузка строится по стандарту PSR-4: префикс пространства имён закреплён за каталогом, и файл класса находится по имени без ручных require_once.
Годы поддержки старой ветки и воспроизводимость сборки
Крупный проект живёт ветками: релизная линия этого года, прошлого, хотфиксы на обеих. Composer относится к этому спокойно, пока каждая линия хранит свой composer.lock. Замок коммитится в репозиторий всегда, каталог vendor не коммитится почти никогда: восстановление окружения на старой ветке сводится к одной команде install, а не к археологии по журналам изменений. Отдельная тонкость достаётся слияниям веток: конфликт в замке означает, что объединять надо манифесты, а замок пересчитывать заново, и ручная правка lock-файла считается дурным тоном.
Платформенные требования php и ext-json в манифесте несут отдельную службу: решатель не выберет версию пакета, которой не будет на целевом сервере, а команда check-platform-reqs проверит соответствие перед релизом. Команда bump поднимает заявленные ограничения до фактически установленных версий, что экономит время мейнтейнерам библиотек.
Безопасность встроена в поток. Команда audit сверяет установленные пакеты с общественной базой извещений об уязвимостях и возвращает ненулевой код при находках, поэтому её ставят в конвейер CI:
composer audit
composer why-not php 8.3
composer check-platform-reqs
Фреймворки держат ритм: Symfony сопровождает LTS-ветки с четырьмя годами заплаток безопасности, Laravel выходит с ежегодными мажорами и подробными гайдами обновления. Сам Composer обновляет себя командой self-update, и переход с 1.0 на 2.0 в 2020 году прошёл для проектов без ломки, обратная совместимость инструмента держится четырнадцатый год.
Хуки манифеста закрывают бытовую рутину: секция scripts выполняет команды на событиях установки и обновления, а плагины встраиваются в сам процесс резолва, заменяя десяток самописных сценариев развёртывания. Рутину выполняет сам установщик, и она не забывается в час ночи перед релизом.
Автозагрузка PSR-4 и ускорение загрузки классов в крупном коде
В крупном проекте автозагрузка это не мелочь, а горячий путь: тысячи классов дергают её на каждом запросе, и каждый промах карты запускает обход файловой системы. PSR-4 отображает префикс пространства имён на каталог, classmap индексирует легаси-файлы без стандартов, а авторитетный режим запрещает искать классы вне карты:
composer dump-autoload --optimize --classmap-authoritative --apcu
Флаг --apcu кладёт карту классов в общую память процессов и требует установленного расширения APCu на сервере, --classmap-authoritative требует от карты полноты и ускоряет длинные цепочки. После появления новых классов карту пересобирают, и в регламенте развёртывания это одна и та же строка. Монорепозитории собираются через path-репозитории, когда внутренние пакеты живут в своих каталогах одного дерева, а замок всё равно держит их вместе:
{
"repositories": [
{"type": "path", "url": "./packages/*"}
]
}
Такой набор закрывает и легаси с классами по старинке, и микросервисы внутри одного репозитория, без потери воспроизводимости.
Приватные пакеты и корпоративные зеркала без публичного реестра
Приватный код в общий реестр не выкладывают, и Composer решает это штатно. Источник типа vcs подключает git-репозиторий напрямую, токен доступа хранится в auth.json, который не коммитят в общий репозиторий:
{
"repositories": [
{
"type": "vcs",
"url": "Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript. :billing/sdk.git"
}
]
}
Компаниям этого часто мало, и строят зеркала. Satis читает собственный список источников и генерирует статический репозиторий из отобранных пакетов:
{
"repositories": [{"type": "vcs", "url": "Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript. :billing/sdk.git"}],
"require": {"billing/sdk": "*"}
}
Коммерческие сервисы добавляют к зеркалу права доступа и кэш артефактов, а за закрытым периметром сети настраиваются переменными окружения и промежуточными шлюзами, и токены не попадают ни в git, ни в логи сборки. Внутренние пакеты при этом ставятся теми же командами, что и публичные, фиксируются тем же замком и проходят ту же проверку на уязвимости. Команда разработки не замечает границы между корпоративным и открытым кодом, и это лучший показатель зрелости инструмента.
Зеркала публичного реестра решают обратную задачу: корпоративный кэш качает пакет один раз, и сотня сборочных машин берёт архивы изнутри периметра. Скорость развёртывания перестаёт зависеть от внешней сети, а сборка старой ветки находит архивы в местном кэше даже после исчезновения внешнего источника.
Гигиена зависимостей в крупном проекте по расписанию
Стабильность через годы не возникает сама, она следует расписанию. Роботы по расписанию открывают пул-реквесты с обновлениями, а люди утверждают их после зелёных тестов. Зелёный конвейер с новым минором, впрочем, не гарантирует, что не изменилось поведение на границах, поэтому решение остаётся за инженером, а расписание выглядит так:
1. раз в неделю список свежих минорных версий просматривается командой composer outdated --direct, и обновления принимаются осознанно;
2. каждый запуск конвейера гоняет composer audit, уязвимость в зависимости блокирует сборку как красный тест;
3. раз в квартал замок пересчитывается целиком, и новый набор миноров проходит полную проверку на всех окружениях;
4. мажорные обновления планируются отдельной веткой по гайду фреймворка, с запасом времени и планом отката.
PEAR проиграл не потому, что идея была плохой, а потому что ручной труд не масштабируется на десятки проектов и годы сопровождения. Composer выиграл двумя файлами и одной привычкой: манифест объявляет желания, замок фиксирует реальность, install воспроизводит её где угодно. Реестр на 469 тысяч пакетов, проверка уязвимостей одной командой и предсказуемые расписания фреймворков превратили экосистему PHP из набора надежд в инженерную среду, где ветка трёхлетней давности собирается без сюрпризов. Стабильность зависимостей это не свойство пакетов, а дисциплина вокруг двух файлов, и в крупном проекте она стоит в расписании наравне с релизами.
Вопрос воспроизводимости при этом решает не инструмент, а обычай: замок в репозитории, install в конвейере, update по расписанию. Различие между сборкой на машине разработчика и на боевом сервере исчезает вместе с главной болью эпохи PEAR.