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 Python CI — Python 项目的同类流水线
- GitHub Actions 矩阵策略 — matrix 的完整玩法与 include 技巧
- GitHub Actions 缓存优化 — 缓存键设计与命中率调优
- GitHub Actions 测试覆盖率集成 — 覆盖率门禁与报告聚合
- GitHub Actions 部署 Vercel — 部署类流水线的对照参考
- PHP 专题 — PHP 语言与工程实践
- DevOps 专题 — CI/CD 通用原则
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。