GitHub Actions PHP CI:Composer、PHPUnit、PHPStan 与多版本矩阵部署

GitHub Actions PHP CI 实战:基于 shivammathur/setup-php 的多版本环境搭建、Composer 依赖安装与 lock 缓存、PHPUnit 测试与覆盖率门禁、PHPStan 与 Psalm 静态分析、PHP-CS-Fixer 与 Pint 代码风格检查、多版本矩阵与 include 扩展、Laravel 项目测试以及基于 Deployer 的自动部署

PHP 项目的 CI 有一个绕不开的矛盾:Composer 依赖安装慢、xdebug 拖垮测试速度、PHPStan 动辄内存溢出,而团队又希望每次 push 都能跑完静态分析与覆盖率。本文用 GitHub Actions 搭建一套完整的 PHP CI 流水线,覆盖 Composer 缓存、PHPUnit 覆盖率、PHPStan/Psalm 静态分析、代码风格检查、PHP 8.1 至 8.3 多版本矩阵,以及 Laravel 项目的测试与 Deployer 部署。


一、基础流水线:环境搭建与 Composer 缓存

1.1 最小可用工作流

# .github/workflows/ci.yml
name: PHP CI

on:
  push:
    branches: [main, master]
  pull_request:
    branches: [main, master]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: "8.3"
          extensions: mbstring, intl, bcmath, pdo_mysql, pdo_sqlite
          ini-values: memory_limit=512M
          tools: composer:v2
          coverage: none

      - name: Validate composer.json
        run: composer validate --strict --no-check-publish

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

      - name: Run tests
        run: vendor/bin/phpunit

1.2 setup-php 关键参数

php-version   目标 PHP 版本,支持 8.1 / 8.2 / 8.3 / 8.4,也支持 latest
extensions    逗号分隔的扩展列表,会自动安装并启用
ini-values    覆盖 php.ini,常用于抬高 memory_limit
tools         预装工具,如 composer:v2、phpunit、phpstan、php-cs-fixer
coverage      none / xdebug / pcov,决定是否安装覆盖率驱动

shivammathur/setup-php 同时支持 Ubuntu、macOS 与 Windows 三种 runner,是 PHP CI 事实上的标准入口。相比手写 apt-get install php8.3-cli,它能自动处理扩展版本匹配、工具预装与缓存,省掉大量样板代码。

1.3 Composer 缓存三件套

Composer 下载慢是 PHP CI 最大的时间黑洞。缓存必须落在 Composer 自己的 cache 目录上,而不是 vendor/——缓存 vendor/ 在依赖变更时极易产生脏状态。

      - name: Get Composer cache directory
        id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"

      - name: Cache Composer packages
        uses: actions/cache@v4
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: ${{ runner.os }}-php-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-php-${{ matrix.php }}-

缓存键用 hashFiles('**/composer.lock') 而不是 composer.json:lock 文件才真正决定了解析出的精确版本,用 json 会导致「依赖没变但缓存命中错误包」的诡异问题。restore-keys 前缀兜底,让 lock 变更后仍能部分复用旧缓存。


二、PHPUnit 测试与覆盖率

2.1 运行测试

      - name: Run test suite
        run: vendor/bin/phpunit --testsuite=unit,feature

      - name: Run with coverage
        if: matrix.coverage != 'none'
        run: vendor/bin/phpunit --coverage-clover build/logs/clover.xml --coverage-text

phpunit.xml 中建议开启 cacheDirectory=".phpunit.cache",配合缓存可显著减少重复解析注解的开销。

2.2 覆盖率驱动选型:xdebug 与 pcov

xdebug   功能全(调试/追踪/覆盖率),但开销极大,测试通常慢 3~5 倍
pcov     专为覆盖率而生,无调试能力,但速度快 5~10 倍
none     不装驱动,只跑测试,最快

结论:CI 只跑覆盖率时用 pcov;需要 step debug 时才用 xdebug

切换驱动只需改一个参数,无需改任何代码:

        with:
          php-version: ${{ matrix.php }}
          coverage: pcov   # 或 xdebug / none

2.3 覆盖率门禁与上传

      - name: Upload coverage to Codecov
        if: matrix.coverage == 'pcov'
        uses: codecov/codecov-action@v4
        with:
          files: ./build/logs/clover.xml
          fail_ci_if_error: true
          token: ${{ secrets.CODECOV_TOKEN }}

若不想引入第三方服务,也可以用 --coverage-text 配合脚本解析输出,或用 phpunit --fail-on-warning 之类的原生门禁。


三、静态分析:PHPStan 与 Psalm

3.1 PHPStan 基础配置

      - name: Run PHPStan
        run: vendor/bin/phpstan analyse --configuration=phpstan.neon --memory-limit=1G
# phpstan.neon
parameters:
  level: 6
  paths:
    - src
    - tests
  excludePaths:
    - src/Generated/*
  treatPhpDocTypesAsCertain: false
  checkMissingIterableValueType: false

level 从 0 到 9 逐级收紧。新项目建议直接从 6 起步;存量项目从 0 或 1 开始,用 baseline 冻结历史问题后逐级提升。

3.2 baseline 与渐进式收紧

# 生成 baseline,把当前所有错误冻结进文件
vendor/bin/phpstan analyse --generate-baseline

# 之后每次 CI 只对「新增错误」报错
vendor/bin/phpstan analyse --configuration=phpstan.neon

baseline 文件必须提交到仓库。它是「技术债清单」,随修复逐步删减条目,而不是永久豁免——建议每个迭代强制减少若干条。

3.3 让错误在 PR 内联标注

      - name: PHPStan with GitHub format
        run: vendor/bin/phpstan analyse --error-format=github --no-progress

--error-format=github 会输出 GitHub 的 workflow command 格式,静态分析错误直接以行内注释的形式出现在 PR 的 Files changed 页签上,审查者不必再翻 CI 日志。

3.4 Psalm 作为替代

      - name: Run Psalm
        run: vendor/bin/psalm --output-format=github --no-cache

Psalm 的类型推断在某些泛型与模板场景下更强,但配置更复杂、误报也更多。团队通常二选一,不必同时上两个静态分析器,否则修错成本翻倍。


四、代码风格:PHP-CS-Fixer 与 Pint

4.1 PHP-CS-Fixer 检查模式

      - name: Check code style
        run: vendor/bin/php-cs-fixer fix --dry-run --diff --verbose

--dry-run 表示只检查不修改,--diff 会打印出具体的改动内容。CI 中绝不能省略 --dry-run,否则流水线会「顺手」修改工作区文件却不提交,产生难以排查的假绿。

4.2 Laravel Pint

Laravel 项目通常直接用 Pint,它是 PHP-CS-Fixer 的封装,零配置即可用:

      - name: Run Pint
        run: vendor/bin/pint --test

--test 等价于 dry-run。本地修复则去掉 --test 直接 vendor/bin/pint,它会按 Laravel 官方风格自动改写。

4.3 缓存与并行

      - name: Cache PHP-CS-Fixer
        uses: actions/cache@v4
        with:
          path: .php-cs-fixer.cache
          key: ${{ runner.os }}-php-cs-fixer-${{ hashFiles('.php-cs-fixer.php') }}

PHP-CS-Fixer 会基于文件哈希跳过未变更文件,缓存 .php-cs-fixer.cache 后,在只改少量文件的 PR 上可以秒级完成。


五、多版本矩阵与 include 扩展

5.1 基础矩阵

    strategy:
      fail-fast: false
      matrix:
        php: ["8.1", "8.2", "8.3"]
        dependencies: [lowest, highest]

fail-fast: false 很重要:默认某个版本失败会立刻取消其他运行中的任务,导致你只看到第一个错误。关掉后能一次性看到全部版本的失败情况。

5.2 用 include 附加参数

矩阵的 include 可以在特定组合上追加变量,这是处理「只在某个版本跑覆盖率」的标准手法:

      matrix:
        php: ["8.1", "8.2", "8.3"]
        include:
          - php: "8.3"
            coverage: pcov
            composer-flags: "--prefer-stable"
          - php: "8.1"
            coverage: none
            composer-flags: "--prefer-lowest"
      - name: Install dependencies
        run: composer update ${{ matrix.composer-flags }} --prefer-dist --no-progress

      - name: Setup coverage
        if: matrix.coverage == 'pcov'
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          coverage: pcov

--prefer-lowest 是 PHP 生态的一个好习惯:它会装依赖声明的最低版本,能提前暴露「依赖升级导致 API 不兼容」的问题,也就是常说的最低版本测试。

5.3 允许失败的实验版本

        include:
          - php: "8.4"
            experimental: true
    continue-on-error: ${{ matrix.experimental == true }}

PHP 8.4 尚未正式支持时,用 experimental 标记并配合 continue-on-error,可以让流水线提前预警兼容性问题,但不阻塞合并。


六、Laravel 项目实战与部署

6.1 环境准备:.env.testing 与 APP_KEY

Laravel 的测试需要一个有效的 APP_KEY,且不应依赖仓库中的 .env。CI 中的标准做法是复制测试环境文件并即时生成密钥:

      - name: Prepare Laravel environment
        run: |
          cp .env.testing .env
          php artisan key:generate --no-interaction

      - name: Run Laravel tests
        run: php artisan test --parallel

.env.testing 中把 DB_CONNECTION 设为 sqlite、DB_DATABASE 设为 :memory:,就能获得一个无需外部服务的内存数据库,测试速度快且完全隔离。

6.2 缓存配置与目录权限

      - name: Clear and cache config
        run: |
          php artisan config:clear
          php artisan config:cache

      - name: Fix storage permissions
        run: chmod -R 777 storage bootstrap/cache

config:cache 在 CI 中要谨慎:它会冻结环境变量,若测试依赖运行时读取 env,缓存后反而拿不到值,通常在测试前 config:clear 更稳妥。

6.3 部署与 environment 保护

  deploy:
    needs: test
    runs-on: ubuntu-latest
    environment: production
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: "8.3"
          tools: deployer

      - name: Deploy with Deployer
        run: dep deploy production --no-interaction
        env:
          SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}

配合 environment: production,可以在仓库设置中配置必需审查者与等待时间,让生产部署在任务实际执行前必须经过人工批准。私钥、主机、数据库口令一律走 secrets,绝不写进 workflow 文件。


七、常见踩坑与模板速查

7.1 踩坑清单

xdebug 拖慢测试        CI 中把 coverage 设为 pcov 或 none,别默认 xdebug
Composer 缓存失效      键用 hashFiles('**/composer.lock'),不要用 composer.json
PHPStan 内存溢出       加 --memory-limit=1G,或抬高 ini-values 的 memory_limit
matrix fail-fast       默认 true 会取消其他版本,排查时先设 false
php-version 与扩展冲突  extensions 里的扩展名要与 PHP 版本匹配,如 8.4 上 gd 行为有变
vendor/ 缓存脏状态     缓存 Composer cache-files-dir,不要缓存 vendor 目录
--dry-run 缺失         风格检查必须 dry-run,否则会静默改写工作区
artisan test 并行冲突   --parallel 需要 paratest,数据库要用内存库避免互相干扰

7.2 完整模板

# .github/workflows/ci.yml 最小可用 PHP CI
name: PHP CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        php: ["8.1", "8.2", "8.3"]
    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mbstring, intl, bcmath, pdo_sqlite
          tools: composer:v2
          coverage: pcov

      - name: Cache Composer
        uses: actions/cache@v4
        with:
          path: ~/.cache/composer
          key: ${{ runner.os }}-php-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}

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

      - name: Static analysis
        run: vendor/bin/phpstan analyse --error-format=github --memory-limit=1G

      - name: Code style
        run: vendor/bin/php-cs-fixer fix --dry-run --diff

      - name: Tests
        run: vendor/bin/phpunit --coverage-clover build/logs/clover.xml

总结

PHP 项目的 GitHub Actions CI 核心链路是:检出代码 → shivammathur/setup-php 装环境 → 缓存 Composer 的 cache 目录 → composer install --prefer-dist → PHPStan 静态分析(--error-format=github 内联标注)→ PHP-CS-Fixer/Pint 风格检查(必须 dry-run)→ PHPUnit 测试与覆盖率。矩阵覆盖 PHP 8.1 到 8.3,用 fail-fast: false 一次看全所有失败,用 include 只在单个版本上开覆盖率以省时间。覆盖率驱动优先选 pcov 而不是 xdebug,速度差距在 5 倍以上。Laravel 项目用 .env.testing 加 sqlite 内存库隔离测试,部署走 Deployer 配合 environment 人工审批与 secrets 保管私钥。把静态分析与风格检查放在测试之前,能在最便宜的一步就拦下大部分低级问题。

延伸阅读:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「github-actions」更多文章

  1. GitHub Actions 本地调试与排错:act、workflow_dispatch、日志与重跑
  2. GitHub Actions 工作流性能与并发控制:concurrency、超时与分钟数优化
  3. GitHub Actions 容器作业与 Service 容器:job container、健康检查与集成测试