golang

Как я ускорил установку PHP-зависимостей в 5 раз с помощью Go

  • вторник, 23 декабря 2025 г. в 00:00:16
https://habr.com/ru/articles/979168/

TL;DR

Переписал Composer на Go, получил 3-5x ускорение благодаря параллельной загрузке пакетов и отсутствию PHP runtime overhead. Проект полностью совместим с экосистемой Composer/Packagist (почти, об этом будет подробнее внизу).

Почему Composer медленный?

Каждый PHP‑разработчик знаком с этим чувством: запускаешь composer install и идёшь заваривать чай. Для небольшого проекта — минута, для Symfony/Laravel — несколько минут. В CI/CD пайплайне это превращается в существенные затраты времени.

Основные проблемы PHP Composer:

  1. Интерпретируемый язык — PHP не может конкурировать с компилируемым Go по скорости выполнения

  2. Последовательная загрузка — пакеты загружаются один за другим

  3. Тяжёлый runtime — даже для простой операции нужен весь PHP‑стек

  4. Сложное разрешение зависимостей — алгоритм SAT‑solver работает медленно

Я решил проверить гипотезу: можно ли существенно ускорить установку зависимостей, используя конкурентную модель Go?


Архитектура go-composer

go-composer/
├── main.go              # Точка входа
├── cmd/                 # CLI команды
│   ├── root.go          # Корневая команда
│   ├── init.go          # go-composer init
│   ├── install.go       # go-composer install
│   ├── update.go        # go-composer update
│   └── require.go       # go-composer require
├── pkg/
│   ├── composer/        # Парсинг composer.json/lock
│   ├── packagist/       # API клиент Packagist
│   ├── resolver/        # Разрешение зависимостей
│   ├── installer/       # Параллельная установка
│   └── autoload/        # Генерация autoload
└── examples/            # Примеры проектов

Диаграмма потока данных

┌─────────────────┐
│  composer.json  │
└────────┬────────┘
         │
         ▼
┌─────────────────┐     ┌─────────────────┐
│    Resolver     │────▶│   Packagist API │
│  (semver logic) │◀────│   (p2/*.json)   │
└────────┬────────┘     └─────────────────┘
         │
         │ Граф зависимостей
         ▼
┌─────────────────┐
│   Installer     │
│ (goroutines)    │
└────────┬────────┘
         │
    ┌────┴────┬────────┬────────┐
    ▼         ▼        ▼        ▼
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ pkg1 │ │ pkg2 │ │ pkg3 │ │ pkgN │  ← Параллельная загрузка
└──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘
   └────────┴────────┴────────┘
            │
            ▼
┌─────────────────┐
│    Autoload     │
│   Generator     │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  vendor/        │
│  ├── autoload   │
│  ├── composer/  │
│  └── packages/  │
└─────────────────┘

Ключевые компоненты

1. Packagist Client — Гибкий парсинг API

Одна из главных проблем при работе с Packagist API — нестандартные ответы. Поле require-dev может быть объектом, строкой или null. Я решил это с помощью кастомного UnmarshalJSON:

Это позволяет корректно обрабатывать ~99.9% пакетов из Packagist, включая те, где метаданные имеют нестандартный формат.

2. Dependency Resolver — Умное разрешение версий

Resolver поддерживает все стандартные Composer constraints:

Оператор

Пример

Значение

^

^2.3

>=2.3.0 <3.0.0

~

~2.3

>=2.3.0 <2.4.0

>=

>=1.0

>=1.0.0

||

^2.0 || ^3.0

Любая из веток

*

*

Любая версия

Алгоритм разрешения
Алгоритм разрешения

3. Parallel Installer — Сердце производительности

Главная фишка go‑composer — параллельная загрузка всех пакетов через горутины:

Что происходит внутри installPackage:

  1. Загрузка ZIP‑архива с Packagist

  2. Проверка SHA-256 контрольной суммы

  3. Распаковка в vendor/{vendor}/{package}/

  4. Формирование данных для go-composer.lock

4. Autoload Generator

Генератор создаёт все необходимые файлы для совместимости с экосистемой Composer

vendor/
├── autoload.php              # Главный autoload файл
├── ClassLoader.php           # PSR-4/PSR-0 загрузчик классов
├── autoload_runtime.php      # Для Symfony Runtime
└── composer/
    ├── installed.json        # Список установленных пакетов
    ├── InstalledVersions.php # API Composer\InstalledVersions
    ├── platform_check.php    # Проверка версии PHP
    └── autoload_classmap.php # Classmap автозагрузка
Поиск bootstrap файлов
Поиск bootstrap файлов

📊 Бенчмарки

Тестирование проводилось на MacBook Pro M1 с интернет‑соединением 300 Mbps.

Проект

Пакеты

PHP Composer

go-composer

Ускорение

Monolog only

3

5-8 сек

2 сек

3-4x

Symfony App

36

15-20 сек

3-5 сек

4-5x

Laravel App

80+

45-60 сек

12-15 сек

4-5x

Почему такое ускорение?

  1. Параллелизм: При 36 пакетах вместо 36 последовательных загрузок происходят ~36 параллельных

  2. Компилируемый код: Go на порядок быстрее PHP для CPU‑bound операций

  3. Нет runtime overhead: Один бинарник 10MB vs PHP interpreter + множество файлов Composer

  4. Эффективная работа с памятью: Go управляет памятью более эффективно

Что сейчас поддерживается

Core функционал

  •  composer.json — полный парсинг (require, autoload, authors и так далее)

  • composer.lock — чтение (пока нет полной совместимости, поэтому не генерируется, а только читается)

  • go-composer.lock — генерация

  • Packagist API — интеграция через P2 API

  • Semver constraints — полная поддержка (^, ~, >=, ||, |, *)

  • Рекурсивное разрешение зависимостей

  • SHA-256 проверка целостности

Autoloading

  • PSR-4 автозагрузка

  • PSR-0 автозагрузка (legacy)

  • Classmap

  • Files

  • Автоматическое подключение bootstrap файлов

Виртуальные пакеты

  • php — версия PHP (пропускается)

  • ext-* — расширения PHP (пропускаются)

  • lib-* — системные библиотеки (пропускаются)

  • composer-runtime-api / composer-plugin-api


Почему go-composer.lock а не composer.lock

Тут все просто, в данный момент не реализована полная совместимость генерируемых lock файлов, поэтому при установке «голого» проекта без composer.lock, собирается свой go‑composer.lock и используется. Если же в проекте уже существует composer.lock, то библиотеки будут собираться из него и go‑composer.lock создаваться не будет. Можно поиграться параметрами new-lock и force-new-lock. Они позволяют управлять поведением работы с go‑composer.lock. По умолчанию new-lock установлен в true и это значит что мы будем пытаться создать go‑composer.lock всегда, если нет composer.lock. Что касается force-new-lock, данный флаг позволяет игнорировать наличие compose.lock и независимо от его существования соберет go‑composer.lock и установит из него зависимости.

Использование

git clone https://github.com/xman12/go-composer.git
cd go-composer
make build
sudo make install

После сборки создается бинарник go‑composer. Так же для удобства добавил команду build-all она собирает под разные платформы бинарники и складывает в папку bin

После чего можно перейти в папку examples/simple-monolog и выполнить команду

go-composer install

Пример использования
Пример использования

Известные ограничения

Ограничение

Статус

Комментарий

Composer scripts

Scripts требуют PHP runtime

Composer plugins

Плагины — это PHP-код

VCS repositories

Только Packagist

Private Packagist

Требуется авторизация, не поддерживается в данный момент

Platform validation

🟡

Определяются, но не валидируются

composer.lock

❌🟡

Нет полной обратной совместимости

Планы на ближайшее будущее

  1. Реализовать полную обратную совместимость с composer.lock

  2. Git repositories — установка напрямую из VCS

  3. Private Packagist — поддержка приватных репозиториев

  4. Хочется еще как‑то ускорить, подумаю над кэшами и как их реализовать.

Подводя итоги

go‑composer демонстрирует, что переход на компилируемый язык с хорошей поддержкой конкурентности даёт существенный прирост производительности для I/O‑bound операций. Были протестированы проекты на Symfony, Laravel и они запустились). Прирост производительности очень вдохновляет продолжать заниматься проектом, но сейчас в проекте хватает над чем работать и текущее состояние не позволяет его использовать в продакшене, по крайней мере я вам это не рекомендую, так‑как возможны сайдэффекты, но работа над проектом продолжается и я нацелен довести проект до полной совместимости что позволит его использовать просто заменив один инструмент на другой. Очень буду благодарен за любую обратную связь.

Github