Что проверяет CI в PHP-проекте

CI, continuous integration, — это автоматический прогон проверок на чистой копии проекта: после push, перед merge, иногда перед deploy. В PHP он обычно отвечает на простой вопрос: «Если взять этот commit, поставить зависимости из composer.lock и выполнить стандартные проверки, проект всё ещё собирается?»

CI не заменяет code review. Он убирает из review механические вещи: битый composer.json, расхождение lock-файла, синтаксические ошибки, нарушения style guide, падение тестов, известные уязвимости в зависимостях. Поэтому эта статья находится рядом с composer.lock, install/update и audit, PHPUnit, моки и стабы, PHPDoc, generics и статический анализ и PSR-1, PSR-12 и стиль кода: все эти инструменты сходятся в одном pipeline.

flowchart TD A[Push или pull request] --> B[Checkout чистой копии] B --> C[composer install из composer.lock] C --> D[composer validate] D --> E[composer audit] E --> F[Style check: PHP-CS-Fixer или PHPCS] F --> G[Static analysis: PHPStan или Psalm] G --> H[PHPUnit tests] H --> I{Все checks зелёные?} I -->|да| J[Merge разрешён] I -->|нет| K[Merge блокируется до исправления]
flowchart TD
    A[Push или pull request] --> B[Checkout чистой копии]
    B --> C[composer install из composer.lock]
    C --> D[composer validate]
    D --> E[composer audit]
    E --> F[Style check: PHP-CS-Fixer или PHPCS]
    F --> G[Static analysis: PHPStan или Psalm]
    G --> H[PHPUnit tests]
    H --> I{Все checks зелёные?}
    I -->|да| J[Merge разрешён]
    I -->|нет| K[Merge блокируется до исправления]
Базовый pipeline PHP-проекта: сначала воспроизводимая установка, затем проверки, которые блокируют merge при ошибке.

Типичный порядок проверок

Обычный quality pipeline начинается с установки зависимостей, а не с тестов. Если зависимости не ставятся воспроизводимо, остальные проверки уже не имеют смысла.

composer install --no-interaction --prefer-dist
composer validate --strict
composer audit
vendor/bin/php-cs-fixer check --diff
vendor/bin/phpstan analyse
vendor/bin/phpunit

В реальном проекте команды часто прячут в Composer scripts, чтобы локальный запуск и CI использовали один контракт:

{
  "scripts": {
    "validate": "composer validate --strict",
    "cs": "php-cs-fixer check --diff",
    "stan": "phpstan analyse",
    "test": "phpunit",
    "audit": "composer audit",
    "check": [
      "@validate",
      "@audit",
      "@cs",
      "@stan",
      "@test"
    ]
  }
}

После этого разработчик запускает composer check локально, а CI делает то же самое на сервере. Это снижает расхождение «у меня зелёное, а в CI красное». Если проект использует Psalm вместо PHPStan, команда меняется, но идея остаётся той же: один script для полного набора проверок.

Быстрое повторение
Почему в PHP-проекте quality pipeline обычно начинает с `composer install`, а не с PHPUnit?

Install, update и production-режим

В CI почти всегда нужен composer install, а не composer update. install берёт точные версии из lock-файла, если он есть. update пересчитывает зависимости и меняет composer.lock; это отдельная операция сопровождения, а не обычный шаг перед merge. Подробнее это разобрано в composer.lock, install/update и audit.

Есть важный нюанс с --no-dev. На quality-стадии CI обычно нужны dev-зависимости: PHPUnit, PHPStan, Psalm, PHP-CS-Fixer, PHP_CodeSniffer. Поэтому composer install --no-dev там сломает проверки или сделает их неполными. --no-dev уместен на production build/deploy стадии, где нужны только runtime-пакеты. Там же часто добавляют оптимизацию autoload:

composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader

Если проект зависит от расширений PHP, полезна отдельная проверка platform requirements. Она ловит ситуацию, когда lock-файл собран под один набор ext-*, а CI или production запускается с другим.

Быстрое повторение
В CI перед merge для обычной проверки commit’а нужна команда `composer ____`, а не пересчёт зависимостей.

Линтер, форматер и coding standard

В PHP словом «линтер» часто называют разные инструменты, но они делают не одно и то же.

php -l проверяет синтаксис одного файла. Он не понимает архитектуру проекта, не строит типовую модель и не проверяет стиль. Это дешёвая базовая проверка, но не полноценный quality gate.

PHP-CS-Fixer — форматер и fixer: он умеет привести код к набору правил, например близкому к PSR-12, и в режиме check показать, какие файлы надо исправить, не меняя их в CI. Локально обычно запускают fix, в CI — check.

vendor/bin/php-cs-fixer fix
vendor/bin/php-cs-fixer check --diff

PHP_CodeSniffer (phpcs) работает через standards и sniffs: он проверяет код на соответствие выбранному стандарту. Часто рядом используют phpcbf, который автоматически исправляет часть нарушений.

vendor/bin/phpcs src tests --standard=PSR12
vendor/bin/phpcbf src tests --standard=PSR12

Выбор между PHP-CS-Fixer и PHP_CodeSniffer зависит от команды и проекта. Не обязательно ставить оба. Важно, чтобы правило было одно: style не обсуждается вручную в каждом PR/MR, а воспроизводимо проверяется машиной. Это практическое продолжение статьи PSR-1, PSR-12 и стиль кода.

Быстрое повторение
Почему `php -l` нельзя считать полноценным quality gate для PHP-проекта?

Статический анализ: PHPStan или Psalm

Статический анализ проверяет код без запуска приложения. Он видит несовместимые типы, недостижимые ветки, подозрительные вызовы, неправильные PHPDoc-аннотации, проблемы с generics и array shapes. Это не то же самое, что PHPUnit: тест исполняет выбранные сценарии, а анализатор пытается доказать свойства кода по сигнатурам и потоку значений.

Типичный запуск PHPStan:

vendor/bin/phpstan analyse src tests

Типичный запуск Psalm:

vendor/bin/psalm

На практике уровень строгости повышают постепенно. Легаси-проект редко можно сразу включить на максимум без сотен ошибок. Для этого используют baseline: текущие проблемы фиксируются как известный долг, а новые ошибки начинают блокировать merge. Но baseline не должен становиться мусорной корзиной. Если он только растёт, CI формально зелёный, а качество фактически падает.

Статический анализ особенно полезен рядом с Типы и strict_types, PHPDoc, generics и статический анализ и Неймспейсы и use: чем точнее типы и автозагрузка, тем меньше анализатору приходится угадывать.

PHPUnit в pipeline

PHPUnit обычно запускают после быстрых проверок: если composer validate или style check уже упали, нет смысла тратить время на весь test suite. Но порядок не должен скрывать ошибки: в больших проектах проверки часто разбивают на параллельные jobs — style, static analysis, tests, security.

Минимальный GitHub Actions workflow может выглядеть так:

name: php-checks

on:
  pull_request:
  push:
    branches: [main]

jobs:
  checks:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          coverage: none

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist

      - name: Run project checks
        run: composer check

Это не универсальный шаблон. В Laravel-проекте могут понадобиться .env.testing, SQLite, Redis или миграции. В библиотеке, наоборот, может быть matrix по нескольким версиям PHP, чтобы заранее поймать проблемы из статьи Миграции между версиями PHP.

Security audit и границы автоматизации

composer audit проверяет установленные пакеты по advisories и политикам зависимостей. Его стоит запускать в CI, но он не заменяет security review. Он не найдёт XSS в шаблоне, небезопасный redirect, ручную склейку SQL или неправильную настройку cookies. Для этого нужны проверки уровня кода и знания из статей Prepared statements и SQL injection, XSS, экранирование вывода и шаблоны, CSRF и state-changing запросы и Конфигурация безопасности PHP.

Хорошее правило: CI должен блокировать merge на проблемах, которые команда считает недопустимыми. Для security audit это обычно known vulnerabilities в production-зависимостях. Для abandoned packages политика может быть мягче или жёстче в зависимости от риска, но она должна быть явной, а не случайной.

Что должно блокировать merge

Минимальный набор для современного PHP-проекта:

  • composer validate --strict — структура composer.json и согласованность с lock-файлом.
  • composer install из lock-файла — воспроизводимая установка.
  • Style check — PHP-CS-Fixer или PHP_CodeSniffer.
  • Static analysis — PHPStan или Psalm на согласованном уровне строгости.
  • PHPUnit — unit/integration tests, которые фиксируют поведение.
  • composer audit — известные проблемы зависимостей.

Для production-сервисов добавляют проверки миграций, сборку assets, smoke tests, Docker image scan, деплой в staging. Но ядро остаётся тем же: каждый commit должен проходить один и тот же набор автоматических gate’ов. Тогда review можно тратить на смысл изменений: доменную модель, API, безопасность, читаемость, а не на пробелы, импорты и забытый lock-файл.

См. также

Источники

  1. Composer CLI documentation
  2. Composer scripts documentation
  3. PHPStan Getting Started
  4. Psalm installation documentation
  5. PHPUnit 12.2 installation manual
  6. PHP-FIG PSR-12: Extended Coding Style
  7. PHP-CS-Fixer usage documentation
  8. PHP_CodeSniffer usage wiki