公開日 2026-03-26

Laravelで品質ゲートを敷く(PHPUnit / Larastan / Pint / GitHub Actions)

Laravel 13 の最小 CRUD に Feature Test、Larastan、Pint、GitHub Actions を足し、ローカルと CI で回る最小品質ゲートを作る。

目次

  1. 前提環境
  2. 1. ゴールと非対象
  3. 2. 前記事の CRUD プロジェクトを起点にする
  4. 3. まず Feature Test を置く
  5. 4. Larastan を追加して静的解析を通す
  6. 5. Pint と composer scripts で品質ゲートをまとめる
  7. 6. GitHub Actions で同じゲートを自動実行する
  8. 7. どこまで自動化し、どこから人間レビューに残すか
  9. 8. よくある詰まりと次の一歩

Laravelで最小CRUDを作る までは進められたが、その次に何を自動化すべきかで止まりやすい読者向けです。
ゴールは、前記事の Book CRUD に Feature Test、Larastan、Pint、GitHub Actions を足し、composer qa を日常の入口にすることです。

Larastan は Laravel 向けの静的解析拡張、Pint は Laravel 標準のコード整形ツールです。この記事では PHPUnit と合わせて、ローカルと GitHub Actions の両方で回る最小の品質ゲートをそろえます。

補助導線として先に読んでおくと入りやすい記事:

前提環境

  • Windows 11
  • WSL2(Ubuntu)
  • VS Code(Remote - WSL)
  • Docker Desktop(WSL 連携有効)
  • GitHub.com アカウント(リポジトリは 6 章で作成)

以降のコマンドは、特記がない限り WSL 側ターミナルで実行します。
開始位置は、前記事で作った laravel-minimal-crud-demo です。

1. ゴールと非対象

この記事で到達する状態:

  • Book CRUD に対する Feature Test を置ける
  • LarastanPint をローカル品質ゲートへ追加できる
  • composer qa で整形チェック、静的解析、テストをまとめて回せる
  • GitHub Actions で同じ gate を自動実行できる

今回は「最初の品質ゲートを敷く」ことに絞ります。
coverage、baseline、Pest への移行、docker compose をそのまま使う CI は今回の外側です。

ここで扱わない内容:

  • Pest への移行
  • baseline / ignoreErrors の本格運用
  • coverage や Mutation Testing
  • PostgreSQL などのサービス付き CI
  • 複数 PHP バージョン matrix
  • 認可、N+1、トランザクション設計の深掘り

2. 前記事の CRUD プロジェクトを起点にする

開始位置は前記事のディレクトリです。

cd ~/projects/laravel-minimal-crud-demo
code .
docker compose up -d

新しく Laravel を作り直す話ではありません。
追加対象は tests/phpstan.neon.github/workflows/ci.ymlcomposer scripts です。

Laravel 13 の新規アプリには、最初から phpunit/phpunitlaravel/pint が入っています。
今回あとから追加する中心は Larastan です。既存の 12 -> 13 アップグレード案件なら、phpunit/phpunit:^12.0 まで追従しているかも composer.json で確認してください。

前記事どおり Route::redirect('/', '/books') にしているなら、既定の tests/Feature/ExampleTest.php はそのままでは合いません。
次に差し替える対象が、この tests/Feature/ExampleTest.php です。

3. まず Feature Test を置く

品質ゲートの土台は Feature Test。
Laravel の phpunit.xml には DB_CONNECTION=sqliteDB_DATABASE=:memory: が入っているので、RefreshDatabase を使ってもローカルの database/database.sqlite は使いません。

先に削除するのは、意味の薄いダミー Unit Test です。

rm tests/Unit/ExampleTest.php
touch tests/Unit/.gitkeep

.gitkeep を置くのは、ファイルがなくなったディレクトリを git が追跡しなくなり、CI で tests/Unit が存在しないとして PHPUnit が止まるのを防ぐためです。

tests/Feature/ExampleTest.php/ の redirect 確認用に置き換えます。

tests/Feature/ExampleTest.php:

<?php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_the_application_redirects_to_books(): void
    {
        $response = $this->get('/');

        $response->assertRedirect('/books');
    }
}

続いて追加するのが、CRUD の主要導線を守る BookCrudTest です。

tests/Feature/BookCrudTest.php:

<?php

namespace Tests\Feature;

use App\Models\Book;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class BookCrudTest extends TestCase
{
    use RefreshDatabase;

    public function test_books_index_displays_saved_books(): void
    {
        Book::query()->create([
            'title' => 'Laravel実践',
            'author' => 'Sato',
            'price' => 2800,
        ]);

        $response = $this->get('/books');

        $response->assertOk();
        $response->assertSee('Laravel実践');
    }

    public function test_store_validates_and_redirects_back_on_error(): void
    {
        $response = $this->from('/books/create')->post('/books', [
            'title' => '',
            'author' => 'Sato',
            'price' => -1,
        ]);

        $response->assertRedirect('/books/create');
        $response->assertSessionHasErrors(['title', 'price']);
    }

    public function test_store_update_and_destroy_work(): void
    {
        $storeResponse = $this->post('/books', [
            'title' => 'Laravel入門',
            'author' => 'Suzuki',
            'price' => 2400,
        ]);

        $storeResponse->assertRedirect('/books');
        $this->assertDatabaseHas('books', [
            'title' => 'Laravel入門',
            'author' => 'Suzuki',
            'price' => 2400,
        ]);

        $book = Book::query()->firstOrFail();

        $updateResponse = $this->patch("/books/{$book->id}", [
            'title' => 'Laravel入門 改訂版',
            'author' => 'Suzuki',
            'price' => 2600,
        ]);

        $updateResponse->assertRedirect('/books');
        $this->assertDatabaseHas('books', [
            'id' => $book->id,
            'title' => 'Laravel入門 改訂版',
            'price' => 2600,
        ]);

        $deleteResponse = $this->delete("/books/{$book->id}");

        $deleteResponse->assertRedirect('/books');
        $this->assertDatabaseMissing('books', [
            'id' => $book->id,
        ]);
    }
}

ここで 1 回テストを流します。

docker compose exec app composer test

4 本通れば、ひとまず既知の CRUD 導線は守れている状態。
次に足すのが静的解析です。

composer testで4本通った状態

4. Larastan を追加して静的解析を通す

Laravel アプリに素の PHPStan をそのまま入れるより、Laravel 向け拡張を含む Larastan のほうが入りやすいです。
まずは dev 依存へ追加します。

docker compose exec app composer require --dev larastan/larastan:^3.0

このコマンドで composer.jsoncomposer.lock が両方更新されます。 6 章でリポジトリを push するとき、この 2 つも一緒にコミットしてください。

次に phpstan.neon を作ります。

phpstan.neon:

includes:
    - vendor/larastan/larastan/extension.neon

parameters:
    level: 5
    paths:
        - app
        - routes
        - tests

解析を流します。

docker compose exec app vendor/bin/phpstan analyse --memory-limit=1G
PHPStanの解析結果 No errors

次の章で composer.json の scripts を整えると、composer analyse として呼べるようになります。

ここで最初に引っかかりやすいのが、Laravel 既定の tests/Unit/ExampleTest.php です。
assertTrue(true) だけのダミーテストは、Larastan から見ると「常に true で意味が薄い」ため、3 章で先に消しました。

app だけでなく routestests も含めているのは、実際に壊れやすいのがその 3 つの境目だからです。
Controller の返し方、route の結び方、Feature Test の前提がずれたときに気づきやすくなります。

5. Pint と composer scripts で品質ゲートをまとめる

Pint は Laravel 13 skeleton の既定 dev 依存です。
ここでは composer.jsonscripts を足し、毎回同じ入口で回せるようにします。

composer.jsonscripts ブロックに次の 4 エントリを追加します。 testlaravel/pint は skeleton に既にあるため追加不要です。larastan/larastan は 4 章で追加済みです。

"analyse": "vendor/bin/phpstan analyse --memory-limit=1G",
"format": "vendor/bin/pint",
"format:check": "vendor/bin/pint --test",
"qa": [
    "@composer format:check",
    "@composer analyse",
    "@composer test"
]

format:check は CI 用で、差分を書き換えずにいまのコードが Pint ルールに合っているかだけを確認します。

最初の確認コマンド:

docker compose exec app composer format:check
Pintのformat:checkでordered_importsが落ちた状態

前記事どおりに手で BookController を書いていると、最初は ordered_imports で落ちることがあります。
その場合は自動整形を 1 回。

docker compose exec app composer format
Pintの自動整形でordered_importsが修正された状態

次に、整形チェック、静的解析、テストをまとめて流します。

docker compose exec app composer qa
composer qaで整形・静的解析・テストがすべて通った状態

composer qa が通れば、ローカル側の最小品質ゲートは一旦完成です。
壊れたらまずここで止まり、直してからレビューへ回す流れにできます。

Pintnot writable で止まるときは、前記事の LOCAL_UID / LOCAL_GID 設定を見直してください。
プロジェクトを WSL ホーム配下に置いていない場合も、権限ずれが起きやすくなります。

6. GitHub Actions で同じゲートを自動実行する

ローカルで通した gate は、そのまま GitHub Actions へ載せます。 今回は DB サービスを増やさず、GitHub-hosted runner の PHP で回す最小構成です。Feature Test が :memory: の SQLite を使うため、この形で十分です。

まだ GitHub 上にリポジトリを作成していない場合は、gh コマンドで作成して origin を設定します。

git init
git branch -M main
gh repo create laravel-minimal-crud-demo --public
git remote add origin https://github.com/<ユーザー>/laravel-minimal-crud-demo.git

gh が入っていない場合は、GitHub のウェブ画面からリポジトリを作成し、表示される git remote add コマンドをそのまま実行してください。 以降は、git push origin main でリモートへ送れる状態を前提に進めます。

まずディレクトリを作成します。

mkdir -p .github/workflows

.github/workflows/ci.yml:

name: ci

on:
  push:
  pull_request:

permissions:
  contents: read

jobs:
  qa:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: mbstring, sqlite3, pdo_sqlite
          coverage: none

      - name: Copy environment file
        run: cp .env.example .env

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

      - name: Generate app key
        run: php artisan key:generate

      - name: Run Pint check
        run: composer format:check

      - name: Run Larastan
        run: composer analyse

      - name: Run PHPUnit
        run: composer test

ポイント:

  • extensions: sqlite3, pdo_sqlite は Feature Test がメモリ上の SQLite を使うために必要です。欠けていると composer test で DB 接続時に止まります
  • cp .env.example .env は Artisan コマンドが APP_KEY などを読む前提で .env を要求するためです
  • php artisan key:generate.envAPP_KEY が空の状態だと暗号化処理で止まるため、依存インストールの直後に実行します
  • composer install --prefer-distcomposer.lock を基準に依存を再現します。composer.lock は必ず一緒にコミットしてください
  • php-version はチームで固定している運用版に合わせてください。ここでは Laravel 13 の最低要件にそろえて 8.3 を基準にしています

ファイルを追加したら、関連ファイルをまとめてコミットして push します。

git add .
git commit -m "Add GitHub Actions CI workflow"
git push origin main

GitHub の Actions タブを開き、ci workflow が起動していることを確認します。各 step が緑になれば完了です。

GitHub Actionsのciワークフローがすべてのstepで成功している画面

CI が落ちた場合は、赤くなっている step を開いてログを読みます。step 名とローカル再実行コマンドの対応は次のとおりです。

step 名ローカルで再実行するコマンド
Run Pint checkdocker compose exec app composer format:check
Run Larastandocker compose exec app vendor/bin/phpstan analyse --memory-limit=1G
Run PHPUnitdocker compose exec app composer test

7. どこまで自動化し、どこから人間レビューに残すか

今回の gate で先に落とせるもの:

  • import 順や空白崩れなど、Pint で拾える整形差分
  • Laravel 文脈の明らかな型ずれや無意味なテスト
  • CRUD の主要導線が壊れたときの振る舞い崩れ

人間レビューへ残すもの:

  • 認可や policy の抜け
  • N+1、index 設計、トランザクション境界
  • バリデーションメッセージや UI 文言の妥当性
  • 例外処理、ログ、監視、運用時の戻し方

機械で拾えるものを先に落とすと、人間レビューは「この設計でよいか」「副作用は無いか」に時間を使いやすくなります。
品質ゲートはレビューの代替ではなく、レビューを設計寄りに寄せる前処理です。

8. よくある詰まりと次の一歩

composer format が書き込めないとき:

  • プロジェクトが WSL ホーム配下にあるか確認する
  • LOCAL_UID / LOCAL_GID を export してから docker compose up -d したか確認する

composer analyse でダミーテストが引っかかるとき:

  • tests/Unit/ExampleTest.php が残っていないか確認する
  • composer dump-autoload を実行してからもう一度流す

GitHub Actions で SQLite 周りのエラーが出るとき:

  • setup-phpextensionssqlite3pdo_sqlite が入っているか確認する
  • cp .env.example .envphp artisan key:generate を飛ばしていないか確認する

シリーズ 3/16

このシリーズ

Laravelの基本を最初から通す

  1. 1. Laravel入門(Route / Controller / View / Model 最小構成)
  2. 2. Laravelで最小CRUDを作る(一覧 / 作成 / 編集 / 削除)
  3. 3. Laravelで品質ゲートを敷く(PHPUnit / Larastan / Pint / GitHub Actions) 現在の記事
  4. 4. Laravelで認証を足す(Breeze 最小導入)
  5. 5. Laravelで認可を入れる(Policyで自分の本だけ編集できるようにする)
  6. 6. LaravelでQueueを始める(database queue + worker 最小構成)
  7. 7. Laravelでスケジューラを動かす(Command + Scheduler 最小構成)
  8. 8. Laravelでファイルアップロードを扱う(Storage + validation)
  9. 9. Laravelでリレーションを扱う(User / Book / Category の基本)
  10. 10. Laravelで検索・並び替え・ページネーション付き一覧を作る
  11. 11. LaravelでLivewireを始める
  12. 12. LaravelでLivewire一覧画面を作る(検索・並び替え・ページネーション)
  13. 13. LaravelでSanctum認証APIを作る
  14. 14. LaravelでBlade / Livewire / Inertia をどう使い分けるか
  15. 15. LaravelでInertia + Vue.js を始める
  16. 16. LaravelでInertia + React を始める