Что делает Composer

Composer — стандартный менеджер зависимостей для PHP-проектов. Он отвечает за три вещи: описывает, какие пакеты нужны проекту; вычисляет совместимый набор версий; генерирует autoload-файл, через который код из vendor/ и ваш src/ подключаются без ручных require по всему приложению.

Важно: Composer не является частью языка PHP. Это внешний CLI-инструмент и формат проекта вокруг composer.json, composer.lock и директории vendor/. Но в современной PHP-разработке он почти такой же базовый, как php.ini, Autoloading и PSR-4 и соглашения PSR-1, PSR-12 и стиль кода.

Типичный проект выглядит так:

my-app/
  composer.json
  composer.lock
  public/
    index.php
  src/
    App.php
  tests/
    AppTest.php
  vendor/
    autoload.php
    ...сторонние пакеты...

composer.json коммитят всегда. composer.lock для приложений тоже обычно коммитят, потому что он фиксирует точные версии зависимостей. vendor/ в Git обычно не кладут: его восстанавливают командой Composer.

flowchart TD A[composer.json] --> B[Composer resolve] B --> C[composer.lock] B --> D[vendor/ packages] D --> E[vendor/autoload.php] E --> F[Application code] A --> G[require / require-dev] A --> H[autoload / autoload-dev] A --> I[scripts] J[Packagist or custom repositories] --> B
flowchart TD
    A[composer.json] --> B[Composer resolve]
    B --> C[composer.lock]
    B --> D[vendor/ packages]
    D --> E[vendor/autoload.php]
    E --> F[Application code]
    A --> G[require / require-dev]
    A --> H[autoload / autoload-dev]
    A --> I[scripts]
    J[Packagist or custom repositories] --> B
Как `composer.json` связывает зависимости, lock-файл, `vendor/` и autoload приложения.
Быстрое повторение
Зачем PHP-проекту Composer, если `require` в языке уже существует?

composer.json как контракт проекта

Минимальный composer.json может быть очень коротким:

{
  "require": {
    "php": "^8.3",
    "ext-mbstring": "*",
    "monolog/monolog": "^3.0"
  }
}

Секция require описывает то, без чего проект не запустится в normal runtime. Сюда относятся PHP-версия, обязательные расширения (ext-mbstring, ext-pdo, ext-json) и библиотеки, которые используются в production-коде. Если забыть указать расширение, Composer может успешно установить зависимости на машине, где расширение есть, а потом приложение упадёт в другом окружении. Это рядом с темой Версии PHP и режимы выполнения: Composer проверяет platform requirements, но только если они честно описаны.

Название пакета имеет форму vendor/package: например, monolog/monolog, symfony/console, phpunit/phpunit. Левая часть не обязательно компания; это namespace владельца в экосистеме Composer, чтобы не было конфликта между двумя пакетами с одинаковой короткой идеей вроде json или logger.

Ограничение версии — это не «поставь ровно вот это». Запись ^3.0 означает совместимые релизы в рамках правила Composer для caret constraints: обычно от 3.0.0 до, но не включая, 4.0.0. Для приложений это удобно: можно получать patch/minor-обновления при явном composer update, но не перескочить на следующий major случайно.

Быстрое повторение
Что может пойти не так, если забыть указать `ext-pdo` или `ext-mbstring` в `require`?

require-dev: инструменты разработки отдельно

Зависимости, нужные только разработчикам и CI, кладут в require-dev:

{
  "require": {
    "php": "^8.3",
    "ext-pdo": "*"
  },
  "require-dev": {
    "phpunit/phpunit": "^11.0",
    "phpstan/phpstan": "^2.0",
    "friendsofphp/php-cs-fixer": "^3.0"
  }
}

PHPUnit не нужен пользователю сайта, но нужен команде; статический анализатор не нужен request-response коду, но нужен перед merge. Поэтому composer install --no-dev на production не должен ставить эти пакеты. Это напрямую связано с PHPUnit, моки и стабы, PHPDoc, generics и статический анализ и CI, линтеры и автоматические проверки.

Частая ошибка — положить production-библиотеку в require-dev, потому что «сейчас она нужна только в консольной команде». Если эта команда запускается в production, зависимость должна быть в require. И наоборот: не надо тянуть phpunit/phpunit в production-runtime только потому, что тесты живут в том же репозитории.

Быстрое повторение
В каком случае зависимость должна лежать в `require`, а не в `require-dev`?

Packagist и repositories

Packagist — основной публичный репозиторий Composer-пакетов. Если в composer.json нет секции repositories, Composer ищет пакеты в Packagist. Поэтому команда:

composer require monolog/monolog

добавляет пакет в composer.json, пересчитывает зависимости и скачивает код в vendor/ без отдельного указания URL.

Секция repositories нужна, когда пакет лежит не в Packagist: приватный Composer-репозиторий, VCS-репозиторий, временный fork, корпоративный mirror.

{
  "repositories": [
    {
      "type": "vcs",
      "url": "https://github.com/acme/private-tools"
    }
  ],
  "require": {
    "acme/private-tools": "dev-main"
  }
}

С этим нужно быть аккуратным. Порядок репозиториев значим: Composer смотрит сверху вниз, и кастомный источник может переопределить пакет из Packagist. Для легаси-фикса это удобно; для supply-chain безопасности — место, которое стоит ревьюить так же внимательно, как composer.lock.

Autoload: связь Composer и кода

После установки Composer генерирует vendor/autoload.php. Входной файл приложения обычно подключает именно его:

<?php

require __DIR__ . '/../vendor/autoload.php';

$app = new App\App();
$app->run();

Для собственного кода чаще всего используют PSR-4:

{
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "App\\Tests\\": "tests/"
    }
  }
}

Теперь класс App\Service\Mailer ожидается в файле src/Service/Mailer.php. Тестовый namespace App\Tests\ живёт отдельно и не загрязняет production autoload. После изменения autoload или autoload-dev запускают:

composer dump-autoload

В production часто используют оптимизацию autoload-карты:

composer install --no-dev --optimize-autoloader

Это не заменяет понимания Неймспейсы и use: Composer не «угадывает» неправильный namespace. Он следует правилам autoload-маппинга.

Scripts: короткие команды проекта

scripts — место для команд, которые команда проекта запускает одинаково у всех:

{
  "scripts": {
    "test": "phpunit",
    "analyse": "phpstan analyse src tests",
    "cs": "php-cs-fixer fix --dry-run --diff",
    "check": [
      "@cs",
      "@analyse",
      "@test"
    ]
  }
}

Теперь вместо длинного набора команд можно выполнить:

composer check

Хороший script не прячет магию, а фиксирует договорённость. Если в CI запускается composer check, локально должна запускаться та же команда. Тогда меньше ситуаций «у меня всё прошло, а на merge request упало».

Есть и event scripts: например, post-install-cmd или post-update-cmd. Они могут быть полезны, но их нельзя добавлять бездумно. Composer-скрипты выполняют код во время установки зависимостей, поэтому незнакомый composer.json и незнакомые плагины — это не просто текстовый конфиг, а часть доверенной supply-chain поверхности.

Практичный пример composer.json

Для небольшого веб-приложения без фреймворка разумный старт может выглядеть так:

{
  "name": "acme/mini-app",
  "description": "Small PHP application",
  "type": "project",
  "require": {
    "php": "^8.3",
    "ext-json": "*",
    "ext-pdo": "*",
    "monolog/monolog": "^3.0"
  },
  "require-dev": {
    "phpunit/phpunit": "^11.0",
    "phpstan/phpstan": "^2.0"
  },
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "App\\Tests\\": "tests/"
    }
  },
  "scripts": {
    "test": "phpunit",
    "analyse": "phpstan analyse src tests",
    "check": ["@analyse", "@test"]
  }
}

Здесь видно главное разделение: runtime-зависимости отдельно, dev-инструменты отдельно, namespace-маппинг отдельно, повторяемые проверки отдельно. Уже следующий слой — как Composer выбирает точные версии, чем install отличается от update, почему composer.lock нужен в CI и как работает composer audit; это отдельная тема composer.lock, install/update и audit.

См. также

Источники

  1. Composer: Basic usage
  2. Composer: The composer.json schema
  3. Composer: Repositories
  4. Packagist.org: About
  5. PHP-FIG: PSR-4 Autoloader