Introduction
A CI/CD pipeline runs your tests, static analysis, and code style checks automatically on every push and pull request. This ensures that no broken code reaches your main branch. GitHub Actions is the most popular CI platform for PHP projects, and setting it up takes just a single YAML file.
Key Concepts
- CI (Continuous Integration): The practice of automatically building and testing code every time changes are pushed to a shared repository.
- GitHub Actions: GitHub's built-in CI/CD platform that runs workflows defined in YAML files inside
.github/workflows/. - Matrix Strategy: Running the same test suite across multiple PHP versions or operating systems in parallel.
- Pipeline: A sequence of automated steps (install, lint, test, analyze) that code must pass before it can be merged.
Real World Context
Without CI, developers must manually run tests before merging, and someone always forgets. CI eliminates this risk by making tests a gate: pull requests cannot be merged until all checks pass. Most open-source PHP libraries use GitHub Actions, and it is free for public repositories.
Deep Dive
Basic GitHub Actions Workflow
Create a workflow file at .github/workflows/ci.yml. This runs PHPUnit, PHPStan, and PHP CS Fixer on every push.
yamlname: CI on: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.5' extensions: mbstring, pdo_mysql coverage: xdebug - name: Install dependencies run: composer install --prefer-dist --no-progress - name: Run PHPStan run: ./vendor/bin/phpstan analyse - name: Check code style run: ./vendor/bin/php-cs-fixer fix --dry-run --diff - name: Run tests run: ./vendor/bin/phpunit --coverage-text
This workflow has a single job called test with five steps: checkout the code, set up PHP 8.5, install Composer dependencies, run static analysis, check code style, and run tests with coverage output.
Matrix Testing Across PHP Versions
To ensure your code works on multiple PHP versions, use a matrix strategy.
yamljobs: test: runs-on: ubuntu-latest strategy: matrix: php-version: ['8.3', '8.4', '8.5'] steps: - uses: actions/checkout@v4 - name: Setup PHP ${{ matrix.php-version }} uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} extensions: mbstring, pdo_mysql - name: Install dependencies run: composer install --prefer-dist --no-progress - name: Run tests run: ./vendor/bin/phpunit
This creates three parallel jobs, one for each PHP version. If tests fail on PHP 8.3 but pass on 8.5, you will know immediately which version has the compatibility issue.
Separating Quality Checks
For larger projects, split quality checks into separate jobs so they run in parallel and you can see exactly which step failed.
yamljobs: phpstan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.5' - run: composer install --prefer-dist --no-progress - run: ./vendor/bin/phpstan analyse code-style: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.5' - run: composer install --prefer-dist --no-progress - run: ./vendor/bin/php-cs-fixer fix --dry-run --diff tests: runs-on: ubuntu-latest strategy: matrix: php-version: ['8.3', '8.4', '8.5'] steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - run: composer install --prefer-dist --no-progress - run: ./vendor/bin/phpunit
With separate jobs, PHPStan, PHP CS Fixer, and PHPUnit all run simultaneously. If code style fails, you do not have to wait for tests to finish to see the failure.
Caching Dependencies
Composer installation can be slow. Cache the vendor directory to speed up builds.
yaml- name: Cache Composer dependencies uses: actions/cache@v4 with: path: vendor key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} restore-keys: ${{ runner.os }}-composer- - name: Install dependencies run: composer install --prefer-dist --no-progress
This caches the vendor directory based on the composer.lock hash. If dependencies have not changed, the install step takes seconds instead of minutes.
Common Pitfalls
- Not caching Composer dependencies — Without caching, every CI run downloads all packages from scratch. This wastes time and can hit rate limits. Always cache the vendor directory.
- Running only tests, not static analysis — Tests verify behavior but miss type errors and code smells. A complete CI pipeline includes PHPUnit, PHPStan, and PHP CS Fixer to catch different categories of issues.
Best Practices
- Make CI a required status check — In GitHub repository settings, mark the CI workflow as required for pull request merges. This prevents anyone from merging code that fails tests or analysis.
- Test against the minimum supported PHP version — Always include your lowest supported PHP version in the matrix. Bugs often appear on older versions due to missing features or different behavior.
Summary
- GitHub Actions runs tests and quality checks automatically on every push and pull request.
- A basic CI pipeline includes PHPUnit, PHPStan, and PHP CS Fixer.
- Matrix testing ensures compatibility across multiple PHP versions.
- Separating quality checks into parallel jobs provides faster feedback and clearer error reporting.
- Caching Composer dependencies dramatically speeds up CI builds.
Code Examples
# .github/workflows/ci.yml
name: PHP Quality Assurance
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.3', '8.4', '8.5']
steps:
- uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php-version }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: mbstring, pdo_mysql
coverage: xdebug
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }}
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Run PHPStan
run: ./vendor/bin/phpstan analyse
- name: Check code style
run: ./vendor/bin/php-cs-fixer fix --dry-run --diff
- name: Run tests with coverage
run: ./vendor/bin/phpunit --coverage-text