対象読者: WSL2 + Docker で PHP 開発中で、ローカルの composer validate --strict と composer format:check / composer analyse / composer test を GitHub Actions に載せたい方
主線は GitHub-hosted runner + actions/checkout + shivammathur/setup-php に固定し、PHPUnit / PHPStan / PHP CS Fixer を 1 本の workflow へ載せます。
ローカル確認は既存シリーズに合わせて WSL2 + Docker で進めます。
一方で、CI 側は最初から Docker を持ち込まず、GitHub-hosted runner の PHP で十分なケースに絞ります。
PostgreSQL や独自 extension が必要なケースは、最後に「Docker を使うべき条件」として切り分けます。
前提環境
- Windows 11
- WSL2(Ubuntu)
- VS Code(Remote - WSL)
- Docker Desktop(WSL 連携有効)
- GitHub.com 上に作成済みのリポジトリ(
origin設定済み)
以降のコマンドは、特記がない限り WSL 側ターミナルで実行します。
サービス名は app に固定します。
1. ゴールと非対象
この記事で到達する状態:
composer validate --strict/composer format:check/composer analyse/composer testを GitHub Actions で自動実行できる.github/workflows/ci.ymlを全文で理解しながら置ける- CI が落ちたときに、どの step を見ればよいか判断できる
3 ツールの役割は次のとおりです。
PHP CS Fixer: 整形崩れを落とすPHPStan: 型不整合や静的に見える危険を落とすPHPUnit: 既知の振る舞い崩れを落とす
この記事で扱わない内容:
- PostgreSQL などのサービス付き CI
docker composeをそのまま実行する CI- 複数 PHP バージョン matrix
- dependency cache の最適化
- self-hosted runner / GitHub Enterprise Server 固有の設定
- artifact upload や coverage レポート連携
2. ローカル用の最小デモ環境を作る
WSL 側のシェルで作業します。
Windows 側から始める場合は wsl で Ubuntu に入り、以下を実行してください。
# Windows側から始める場合のみ実行
# wsl
mkdir -p ~/projects/php-github-actions-minimal-ci-demo
cd ~/projects/php-github-actions-minimal-ci-demo
git init
git branch -M main
mkdir -p docker/php src tests
code .
新規フォルダから始めるので、ここでローカル Git リポジトリも初期化しておきます。
まだ GitHub 上にリポジトリを作成していない場合は、gh コマンドで作成して origin を設定します。
gh repo create php-github-actions-minimal-ci-demo --public
git remote add origin https://github.com/<ユーザー名>/php-github-actions-minimal-ci-demo.git
gh が入っていない場合は、GitHub のウェブ画面からリポジトリを作成し、表示される git remote add コマンドをそのまま実行してください。リポジトリ名は上記を参考にしてください。
以降は、git push origin main でリモートへ送れる状態を前提に進めます。
compose.yml を作成します。
services:
app:
build:
context: .
dockerfile: docker/php/Dockerfile
working_dir: /workspace
volumes:
- ./:/workspace
command: ["sleep", "infinity"]
docker/php/Dockerfile を作成します。
今回は php:8.5-cli を土台にし、ローカル側では後で php -m を使って PHPUnit が使う拡張を確認します。
FROM php:8.5-cli
RUN apt-get update \
&& apt-get install -y --no-install-recommends git unzip \
&& rm -rf /var/lib/apt/lists/*
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /workspace
composer.json はこの時点で scripts まで入れておきます。
こうしておくと、後で composer validate --strict を通すときに composer.lock の整合を崩しにくくなります。
{
"name": "example/php-github-actions-minimal-ci-demo",
"description": "Minimal PHP CI demo for GitHub Actions article",
"type": "project",
"license": "MIT",
"require": {
"php": "^8.5"
},
"require-dev": {},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"format:check": "php-cs-fixer fix --dry-run --diff",
"analyse": "phpstan analyse",
"test": "phpunit"
}
}
起動して確認します。
docker compose up -d --build
docker compose exec app php -v
docker compose exec app composer --version
docker compose exec app php -m | grep -E '^(dom|libxml|mbstring|tokenizer|xml|xmlwriter)$'
起動後の確認例:
PHP 8.5.3 (cli)
Composer version 2.9.5
PHPUnit は mbstring だけでなく XML 系拡張も使います。
今回の php:8.5-cli では dom / libxml / mbstring / tokenizer / xml / xmlwriter が有効でした。
ベースイメージや配布 PHP を変える場合は、同じ php -m の確認を省かないでください。
詰まったとき:
- コンテナが起動しない:
docker compose logs app - Dockerfile を変えたのに反映されない:
docker compose up -d --build
3. 3ツールと composer scripts をそろえる
次に、3 ツールを dev 依存として導入します。
docker compose exec app composer require --dev phpunit/phpunit:^13 phpstan/phpstan:^2.1 friendsofphp/php-cs-fixer:^3.89
docker compose exec app composer dump-autoload
docker compose exec app vendor/bin/phpunit --version
docker compose exec app vendor/bin/phpstan --version
docker compose exec app vendor/bin/php-cs-fixer --version
インストール時のバージョン例:
friendsofphp/php-cs-fixer 3.94.2
phpstan/phpstan 2.1.40
phpunit/phpunit 13.0.5
composer require --dev ... を実行すると、composer.json の require-dev と composer.lock が更新されます。
CI の再現性を保つため、composer.lock も composer.json と一緒にコミットしてください。
phpunit.xml を作成します。
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
phpstan.neon を作成します。
parameters:
level: 5
paths:
- src
- tests
.php-cs-fixer.dist.php を作成します。
対象は src と tests に固定します。
<?php
$finder = PhpCsFixer\Finder::create()
->in(__DIR__ . '/src')
->in(__DIR__ . '/tests')
->name('*.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return (new PhpCsFixer\Config())
->setRiskyAllowed(false)
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'single_quote' => true,
'no_trailing_whitespace' => true,
])
->setFinder($finder);
最小のサンプルコードを置きます。
ここでは、入力文字列を整えて挨拶文を返すだけの GreetingService にします。
src/GreetingService.php
<?php
declare(strict_types=1);
namespace App;
final class GreetingService
{
public function format(string $name): string
{
return sprintf('Hello, %s!', trim($name));
}
}
tests/GreetingServiceTest.php
<?php
declare(strict_types=1);
namespace Tests;
use App\GreetingService;
use PHPUnit\Framework\TestCase;
final class GreetingServiceTest extends TestCase
{
public function testFormatReturnsTrimmedGreeting(): void
{
$service = new GreetingService();
self::assertSame('Hello, Taro!', $service->format(' Taro '));
}
}
この時点で composer.json に追加されるのは、主に require-dev と次の scripts です。
"scripts": {
"format:check": "php-cs-fixer fix --dry-run --diff",
"analyse": "phpstan analyse",
"test": "phpunit"
}
format:check でパスを個別に並べず、Finder 側へ寄せているのは、ローカルでも CI でも同じコマンドをそのまま使えるようにするためです。
パス指定を .php-cs-fixer.dist.php 側に集約しておけば、対象ディレクトリを変えてもコマンドは触らずに済みます。
詰まったとき:
vendor/binが見つからない:docker compose exec app ls -la vendor/bin- クラスが見つからない:
docker compose exec app composer dump-autoload format:checkが末尾改行や改行コードで落ちる: ファイル末尾に改行を入れ、VS Code の右下をLFにして保存し直す
4. ローカルで 4つの確認コマンドが通ることを確認する
CI を書く前に、同じコマンド列をローカルで通します。
docker compose exec app composer validate --strict
docker compose exec app composer format:check
docker compose exec app composer analyse
docker compose exec app composer test
成功時の確認例:
./composer.json is valid
Found 0 of 2 files that can be fixed
[OK] No errors
OK (1 test, 1 assertion)
ここで composer validate --strict を最初に置くのは、workflow の書き方以前に composer.json / composer.lock の整合が崩れていないかを先に確認するためです。
ローカルで通らないものを、そのまま CI に載せても切り分けが難しくなります。
詰まったとき:
composer validate --strictで lock 不整合が出る: まずdocker compose exec app composer install --no-interaction --no-progressで再確認するrequired package is not present in the lock file系が出る:docker compose exec app composer update vendor/package-name --no-interaction --no-progressで対象パッケージだけ更新するcontent-hashだけの軽微なずれが出る:docker compose exec app composer update --lock --no-interaction --no-progressでも直せるformat:checkだけ落ちる: まずファイル末尾の改行有無を確認し、そのうえでLF保存と引用符・空白を見直すanalyseが落ちる: 型不整合が出ているファイルへ戻るtestが落ちる: 期待値と実装の文字列を見比べる
5. .github/workflows/ci.yml を作る
ここから GitHub Actions 側です。
この章で実際にやることは、次の 5 手順です。
- ローカルのプロジェクトルートで
.github/workflows/ci.ymlを作る - workflow 全文を貼る
- workflow と関連ファイルをコミットする
- GitHub へ push する
- GitHub の
Actionsタブでciの実行結果を確認する
まずディレクトリを作成します。
mkdir -p .github/workflows
.github/workflows/ci.yml を作成します。各 step の実行順と設定の意図を確認しながら置けるよう、cache や matrix は含まない最小構成にしています。
name: ci
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
qa:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.5'
coverage: none
extensions: mbstring
- name: Validate composer.json
run: composer validate --strict
- name: Install dependencies
run: composer install --no-interaction --no-progress --prefer-dist
- name: Check formatting
run: composer format:check
- name: Run PHPStan
run: composer analyse
- name: Run PHPUnit
run: composer test
コードのポイント
① permissions: contents: read で最小権限にする
permissions:
contents: read
checkout に必要なリポジトリ読み取り権限のみを宣言しています。デフォルトでは書き込み権限も付与されるため、明示的に絞ることで仮にサードパーティ action が侵害された場合のリスクを抑えられます。
② Checkout repository を最初に置く理由
- name: Checkout repository
uses: actions/checkout@v6
setup-php より前にソースコードが存在しないと composer install も動きません。checkout で runner にコードを取得してから PHP セットアップへ進む順序が前提です。actions/checkout@v6 は使用前に リリースページ で最新タグを確認してください。
③ --prefer-dist で composer.lock を基準に再現する
- name: Install dependencies
run: composer install --no-interaction --no-progress --prefer-dist
--prefer-dist を付けると zip パッケージを使い、コミット済みの composer.lock の内容を正確に再現します。--no-interaction と --no-progress は対話確認とプログレス表示を無効にして、CI ログを読みやすくするためです。self-hosted runner や独自イメージでは setup-php が入れる mbstring 以外に dom / xml / xmlwriter も確認してください。
次に、workflow を GitHub へ送るための最小手順をそのまま実行します。 ここでは、ローカルで作ったデモ一式をすでに GitHub リポジトリへ push できる前提で進めます。
git add composer.json composer.lock phpunit.xml phpstan.neon .php-cs-fixer.dist.php src tests .github/workflows/ci.yml
git commit -m "Add minimal PHP CI workflow"
git push origin main
デフォルトブランチが main ではない場合は、workflow の branches: [main] と git push origin main の両方を自分のブランチ名へ読み替えてください。
GitHub に push できたら、ブラウザで対象リポジトリの Actions タブを開き、ci workflow が起動していることを確認します。
この workflow を push する前に、少なくとも次のファイルは一緒にコミットしてください。
composer.jsoncomposer.lockphpunit.xmlphpstan.neon.php-cs-fixer.dist.phpsrc/tests/.github/workflows/ci.yml
6. 失敗ログの見方を覚える
GitHub Actions で落ちたときは、次の順に辿ります。
- GitHub の
Actionsタブを開く - 失敗した workflow run を開く
qajob を開く- 赤くなっている step を開いてログを読む
job 全体をぼんやり眺めるより、落ちた step を先に開くほうが早いです。
step 名とローカル再実行コマンドを対応づけると、復旧もしやすくなります。
| step 名 | まずローカルで再実行するコマンド |
|---|---|
Validate composer.json | docker compose exec app composer validate --strict |
Check formatting | docker compose exec app composer format:check |
Run PHPStan | docker compose exec app composer analyse |
Run PHPUnit | docker compose exec app composer test |
Validate composer.json が落ちる場合は、CI より先に composer.json / composer.lock の整合を見直します。
代表的な失敗例:
Check formatting が落ちた例
1) src/GreetingService.php
---------- begin diff ----------
@@ -8,6 +8,6 @@
- return sprintf("Hello, %s!", trim($name));
+ return sprintf('Hello, %s!', trim($name));
...
Script php-cs-fixer fix --dry-run --diff handling the format:check event returned with error code 8
これは、整形ルールに合っていないだけです。
今回のようにファイル末尾の改行がないだけでも落ちるので、まず末尾に 1 行改行を入れて保存し直します。
そのうえでローカルで composer format:check を再実行します。
Run PHPStan が落ちた例
Line tests/GreetingServiceTest.php
16 Parameter #1 $name of method App\GreetingService::format() expects string, int given.
[ERROR] Found 1 error
これは、型の食い違いです。
対象ファイルへ戻り、引数型や戻り値型を見直します。
Run PHPUnit が落ちた例
There was 1 failure:
1) Tests\GreetingServiceTest::testFormatReturnsTrimmedGreeting
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'Hello, Taro'
+'Hello, Taro!'
これは、実装と期待値の振る舞いがずれています。
仕様としてどちらが正しいかを確認し、実装かテストのどちらを直すか判断します。
7. Docker を使う場合と使わない場合を切り分ける
ローカルが Docker でも、CI まで必ず Docker にする必要はありません。
まずは、次の表で判断すると整理しやすいです。
| 条件 | 今回の非Docker主線で十分 | Docker CI へ進む |
|---|---|---|
| アプリの確認対象 | PHPUnit / PHPStan / PHP CS Fixer だけ | DB 接続、Redis、メール、外部サービス起動待ちが必要 |
| PHP 実行環境 | setup-php の PHP と標準拡張で足りる | 独自 extension、OS パッケージ、システム依存がある |
| ローカルとの差分 | 少し違っても困らない | Dockerfile と CI の差を減らしたい |
| workflow の複雑さ | 最初は読みやすさを優先したい | services や docker compose の手順を含めたい |
次の条件に当てはまるなら、今回の主線で十分です。
- GitHub-hosted runner の PHP だけで品質チェックが完結する
- DB や Redis を立てなくても
testが走る - GitHub-hosted runner と
setup-phpの組み合わせで必要な PHP 拡張を満たせる
逆に、次の条件なら Docker CI 記事へ進んだほうが自然です。
- PostgreSQL / MySQL / Redis などのサービス起動が必要
apt-getや独自 extension を CI でもそろえたい- ローカルの
docker composeと CI の差をできるだけ減らしたい
8. まとめと次の一歩
本記事で、次の状態を作れました。
- ローカルの
composer validate --strictと 3 つの品質チェックを整理できる - GitHub Actions の最小 workflow を全文で置ける
- CI が落ちたときに、step 単位でログを読める
最初の CI は、速さよりも読みやすさを優先したほうが扱いやすいです。
まずはローカルで通る 3 コマンドに composer validate --strict を加えた workflow からで十分です。
ローカルの受け入れ基準から整理したい場合は、AI生成コードを受け入れる最小品質ゲート(PHPStan + PHPUnit + CS Fixer) が先に役立ちます。
DB 付きや docker compose を使う CI へ進む場合は、GitHub Actionsで docker compose を使うPHP CI(PostgreSQL付き) が次のステップになります。