Post

PHP Build with Composer: Build Files, Scanners and CI

How Composer builds PHP applications, which files it needs, how to run static analysis, security scans and tests, and a complete GitHub Actions pipeline.

PHP Build with Composer: Build Files, Scanners and CI

PHP is interpreted, so there is no compile step. The “build” is Composer installing locked dependencies and generating an optimised autoloader, followed by static analysis (PHPStan or Psalm), which does much of the checking a compiler does in other languages. Composer also runs project scripts and audits dependencies for known vulnerabilities.

This post is part of Build Ecosystems by Programming Language.

At a glance

CategoryCommon tools
Build toolsComposer (install, autoload, scripts), Box (PHAR packaging), Phing / Robo (task runners)
Package registryPackagist, Private Packagist, Satis, Artifactory
Build filescomposer.json, composer.lock, auth.json, phpunit.xml.dist, phpstan.neon, psalm.xml, .php-cs-fixer.dist.php, phpcs.xml
Lint and formatPHP_CodeSniffer, PHP-CS-Fixer, Laravel Pint
Static analysisPHPStan, Psalm
SAST (open source)Psalm taint analysis, Semgrep, SonarQube Community Build
SAST (commercial)Checkmarx One, Veracode, Snyk Code, Fortify, SonarQube Server
SCAcomposer audit, Roave Security Advisories, OSV-Scanner, Trivy, Snyk, Mend
FrameworksLaravel, Symfony, Slim, Laminas, WordPress, Drupal
Test and coveragePHPUnit, Pest, Codeception; coverage via PCOV or Xdebug
Build outputSource + vendor/ packaged as a container image, PHAR archive
CI cacheComposer cache directory (composer config cache-files-dir)

How the build works

In CI, each stage of the generic build pipeline maps to a Composer command, plugin or scanner. Press play, or step through it:

  1. Source + composer.lock: A commit triggers CI. composer.json declares dependencies, composer.lock pins them, and composer validate --strict checks the two agree.
  2. Install: composer install --no-interaction --prefer-dist installs exactly composer.lock from Packagist; CI caches Composer's download cache.
  3. Lint and format: vendor/bin/php-cs-fixer fix --dry-run --diff fails on formatting changes; PHP_CodeSniffer and Laravel Pint are alternatives.
  4. Static analysis: PHP has no compile step, so vendor/bin/phpstan analyse finds type errors and bugs before the tests run.
  5. Test and coverage: vendor/bin/phpunit --coverage-text runs the tests, with coverage from PCOV or Xdebug.
  6. Security scans in parallel: vendor/bin/psalm --taint-analysis traces untrusted input to dangerous sinks, composer audit checks dependencies, and Gitleaks scans for secrets.
  7. Quality gate: Tests, coverage and every scanner are required checks, so a single failure stops the pipeline here.
  8. Fail the build (fail): A failed check stops the pipeline at the gate, so nothing is packaged, signed or published.
  9. Production build (pass): composer install --no-dev --optimize-autoloader --classmap-authoritative builds the production vendor/ tree; Box can package a PHAR.
  10. SBOM and signing (pass): Syft generates the SBOM, and Sigstore cosign signs the release.
  11. Ship (pass): The app and its vendor/ directory ship as a container image to a registry; libraries are published through Packagist instead.
  • composer update resolves new versions and rewrites composer.lock; only developers run it.
  • composer install installs exactly what the lockfile says; CI and production only run this.
  • --no-dev --optimize-autoloader (or --classmap-authoritative) removes dev tools and builds a fast classmap for production.
  • Platform requirements: config.platform.php in composer.json makes resolution use the production PHP version, even if a developer runs a newer one locally.

Build files

FilePurposeCommit?
composer.jsonDependencies, autoload rules, scripts, platform configYes
composer.lockExact versions and dist hashesYes (applications)
auth.jsonCredentials for private repositoriesNo
phpunit.xml.distTest suites and coverage configYes
phpstan.neon / psalm.xmlStatic analysis level and rulesYes
.php-cs-fixer.dist.php / phpcs.xmlCoding standardYes
vendor/Installed packagesNo

Commands by category

1
2
3
4
5
6
7
8
composer validate --strict                       # check composer.json / lock consistency
composer install --no-interaction --prefer-dist  # install from lockfile (CI)
vendor/bin/php-cs-fixer fix --dry-run --diff     # format check
vendor/bin/phpstan analyse --memory-limit=1G     # static analysis
vendor/bin/psalm --taint-analysis                # SAST (taint tracking)
composer audit                                   # SCA
vendor/bin/phpunit --coverage-text               # tests + coverage
composer install --no-dev --optimize-autoloader --classmap-authoritative   # production build

Minimal project

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
  "name": "example/api",
  "type": "project",
  "require": {
    "php": "^8.3",
    "slim/slim": "^4.14",
    "slim/psr7": "^1.7"
  },
  "require-dev": {
    "phpunit/phpunit": "^11.0",
    "phpstan/phpstan": "^2.0",
    "friendsofphp/php-cs-fixer": "^3.64"
  },
  "autoload": { "psr-4": { "App\\": "src/" } },
  "autoload-dev": { "psr-4": { "App\\Tests\\": "tests/" } },
  "config": {
    "platform": { "php": "8.3" },
    "sort-packages": true
  },
  "scripts": {
    "lint": "php-cs-fixer fix --dry-run --diff",
    "analyse": "phpstan analyse",
    "test": "phpunit"
  }
}
1
2
3
4
5
# phpstan.neon
parameters:
  level: 6
  paths:
    - src

Frameworks

FrameworkStart a project
Laravelcomposer create-project laravel/laravel app
Symfonycomposer create-project symfony/skeleton app
Slimcomposer create-project slim/slim-skeleton app
WordPress (Bedrock)composer create-project roots/bedrock site

CI with GitHub Actions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
name: php-ci
on: [push, pull_request]
permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          coverage: pcov
          tools: composer:v2

      - name: Composer cache
        id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
      - uses: actions/cache@v4
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: composer-${{ hashFiles('composer.lock') }}

      - run: composer validate --strict
      - run: composer install --no-interaction --prefer-dist
      - run: composer lint
      - run: composer analyse
      - run: composer audit
      - run: vendor/bin/phpunit --coverage-text

Best practices

  • Commit composer.lock and only run composer install outside development.
  • Set config.platform.php to the production PHP version.
  • Raise the PHPStan level gradually and use a baseline file for legacy code instead of lowering the level.
  • Fail on composer audit in CI, and add roave/security-advisories to block installing known vulnerable versions at all.
  • Keep auth.json out of git; pass credentials via COMPOSER_AUTH in CI.
  • Build production images with --no-dev --classmap-authoritative and enable OPcache.
This post is licensed under CC BY 4.0 by the author.