← All posts

Tutorial CI/CD

How to Run Static Analysis on PHP with PHPStan in GitHub Actions

Add PHPStan static analysis to your GitHub Actions CI pipeline and catch PHP type errors before they reach production.

Difficulty Intermediate Time 20 min
Magnifying glass scanning PHP code blocks in a CI pipeline, flagging type errors for static analysisv

I'll walk you through adding PHPStan static analysis to a GitHub Actions workflow so every push and pull request is automatically checked for type errors, dead code, and logic mistakes — without running the app at all. By the end, your CI pipeline will fail fast on any issue PHPStan detects and report exactly which file and line needs fixing.

Prerequisites

  • A GitHub repository with a PHP project.
  • composer.json and composer.lock committed at the repository root.
  • GitHub Actions enabled for the repository.
  • PHP 8.1 or higher recommended (PHPStan works from PHP 7.2, but 8.x gives the best analysis).
  • An ubuntu-latest runner with the default Bash shell.
  • Optional but recommended: complete the Composer caching tutorial first so your workflow doesn't re-download packages on every run.

Step 1 — Install PHPStan as a dev dependency

I add PHPStan to the project's dev dependencies so it lives in version control alongside the code it analyses. I never install it globally on the runner because that would let it drift out of sync with the project.

composer require --dev phpstan/phpstan

This updates both composer.json and composer.lock. Commit both files now so CI uses the exact same version locally.

git add composer.json composer.lock git commit -m "Add PHPStan as a dev dependency"

Step 2 — Create a PHPStan configuration file

PHPStan reads its settings from a phpstan.neon file at the project root. The most important setting to start with is the analysis level. PHPStan has ten levels (0–9): level 0 catches basic errors; level 9 is the strictest. I recommend starting at level 5 — it catches real bugs without flooding a new project with warnings.

cat > phpstan.neon <<'NEON' parameters: level: 5 paths: - src NEON

Replace src with the directory or directories you actually want to analyse, for example app, lib, or a list of both. If your project has no src directory yet, point it at whatever directory holds your PHP files.

Gotcha: If you leave paths empty or omit it, PHPStan tries to analyse everything it can reach, including Composer's own autoloader internals. Always be explicit about which directories to scan.

git add phpstan.neon git commit -m "Add PHPStan configuration at level 5"

Step 3 — Verify PHPStan runs locally

Before wiring up CI, I confirm the analysis passes locally. This also catches misconfigured paths before they silently break the workflow.

./vendor/bin/phpstan analyse

PHPStan reads phpstan.neon automatically from the project root. A clean result looks like this:

 [OK] No errors

If you see errors at this point, fix them now or drop the level temporarily. CI should start green — you can tighten the level incrementally once the workflow is in place.

Note: If PHPStan reports errors you want to address later, you can generate a baseline file with ./vendor/bin/phpstan analyse --generate-baseline. This records existing errors so CI ignores them until you tackle them deliberately. Commit phpstan-baseline.neon and reference it in phpstan.neon with includeFiles: [phpstan-baseline.neon]. Do not use the baseline as an excuse to ignore real bugs — treat it as a temporary debt register.

Step 4 — Create the GitHub Actions workflow

Now I wire everything together in a workflow file. The job checks out the code, sets up PHP, installs dependencies, and runs PHPStan. I include the Composer cache step from the previous tutorial so repeated runs stay fast.

mkdir -p .github/workflowscat > .github/workflows/phpstan.yml <<'YAML' name: PHPStan on: push: pull_request: jobs: analyse: runs-on: ubuntu-latest text steps:  - name: Check out the code    uses: actions/checkout@v4  - name: Set up PHP    uses: shivammathur/setup-php@v2    with:      php-version: '8.3'      coverage: none  - name: Get Composer cache directory    id: composer-cache    run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"  - name: Cache Composer downloads    uses: actions/cache@v4    with:      path: ${{ steps.composer-cache.outputs.dir }}      key: ${{ runner.os }}-php-8.3-composer-${{ hashFiles('composer.lock') }}      restore-keys: |        ${{ runner.os }}-php-8.3-composer-  - name: Install dependencies    run: composer install --prefer-dist --no-interaction --no-progress  - name: Run PHPStan    run: ./vendor/bin/phpstan analyse --no-progress YAML

The --no-progress flag suppresses the animated progress bar, which produces cleaner output in CI logs. PHPStan exits with a non-zero status code when it finds errors, so GitHub Actions fails the job automatically — no extra configuration needed.

Step 5 — Push the workflow and review the output

Commit the workflow file and push it to trigger the first run.

git add .github/workflows/phpstan.yml git commit -m "Add PHPStan static analysis workflow" git push

Open the Actions tab in your GitHub repository and watch the PHPStan workflow run. A passing job ends with output similar to this in the Run PHPStan step:

 [OK] No errors

A failing job lists each offending file, line number, and a plain-English description of the problem, for example:

------ ----------------------------------------------------------------- Line src/Service/OrderProcessor.php ------ ----------------------------------------------------------------- 42 Parameter #1 $id of method OrderRepository::find() expects int, string given. ------ -----------------------------------------------------------------

Fix each error in your local branch, push again, and the workflow re-runs automatically. Pull requests get a status check on every commit, so reviewers can see analysis results inline before merging.

Wrap-up

You now have PHPStan static analysis running automatically in GitHub Actions on every push and pull request. The setup is minimal — a single phpstan.neon config, PHPStan as a Composer dev dependency, and one workflow file. From here you can raise the analysis level gradually, add extension packages like phpstan/phpstan-doctrine or phpstan/phpstan-symfony for framework-aware rules, and make the PHPStan status check required before merging to keep the main branch type-error-free.