Skip to content
TutorialDevOps

Code Quality Gates: Laravel Pint and PHPStan in CI

D

Dinesh Wijethunga

July 25, 2026 3 min readBeginner 19 views
ShareX / TwitterLinkedIn
Code Quality Gates: Laravel Pint and PHPStan in CI

The moment you add a code quality gate to CI, something quietly changes on your team. Developers stop arguing about code style in reviews because the robot handles it. Static-analysis errors stop reaching production because they can't pass the gate. You stop inheriting other people's technical debt because new errors are blocked immediately, even if old ones exist.

That last point is the PHPStan baseline trick. We'll get to it.

The Code Quality Job

This job runs on every push and is intentionally fast — no database, no migrations, just PHP and a vendor folder:

  quality:
    name: Code Quality
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP 8.4
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          extensions: mbstring, pdo, bcmath, zip, intl
          coverage: none
          tools: composer:v2

      - name: Cache Composer dependencies
        uses: actions/cache@v4
        with:
          path: api/vendor
          key: php-8.4-composer-${{ hashFiles('api/composer.lock') }}
          restore-keys: php-8.4-composer-

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

      - name: Check formatting (Laravel Pint)
        run: ./vendor/bin/pint --test

      - name: Static analysis (PHPStan)
        run: ./vendor/bin/phpstan analyse --memory-limit=2G --no-progress

pint --test runs in read-only mode — it checks without modifying files. Exit code 1 if any file has style drift. Run ./vendor/bin/pint locally (without --test) to auto-fix before pushing.

Configuring PHPStan

Add a phpstan.neon to your API root:

parameters:
    level: 8
    paths:
        - app
        - Modules
    excludePaths:
        - vendor
    ignoreErrors:
        - identifier: missingType.iterableValue
        - identifier: missingType.generics

Level 8 is strict. On a fresh project, aim for it from day one. On an existing codebase, start at level 4 and raise it gradually.

The Memory Problem

PHPStan at level 8 on a large codebase needs more than 512MB of RAM. Use --memory-limit=2G in CI. Locally, add a shortcut to composer.json:

"scripts": {
    "analyse": "php -d memory_limit=2G vendor/bin/phpstan analyse --no-progress"
}

The Baseline Trick for Legacy Codebases

Here's the situation I encounter on almost every inherited project: PHPStan level 8 reports 2,600 existing errors. You can't fix them all in one PR. But you also can't ignore level 8 — you need it to catch new errors.

The solution is a baseline file. You generate it once. It records all current errors as "accepted." From that point, any new error fails CI, but existing ones are silenced:

# Run once locally, commit the result
php -d memory_limit=2G vendor/bin/phpstan analyse --generate-baseline phpstan-baseline.neon

Then include it in phpstan.neon:

includes:
    - phpstan-baseline.neon

parameters:
    level: 8
    paths:
        - app
        - Modules

Commit phpstan-baseline.neon. From now on: CI passes on existing debt, fails on new debt. As you fix old errors, re-generate the baseline with fewer entries. Eventually you won't need it.

Ordering Jobs: Quality Before Tests

Use needs: to enforce ordering:

  tests:
    name: Tests (PHP 8.4, MySQL 8.4)
    needs: quality   # this line
    runs-on: ubuntu-latest

Now the test job only starts if quality passed. This saves CI minutes — there's no point running a 3-minute test suite against code that won't pass style checks.

Developer Workflow With These Gates

The new routine before any push:

# Fix style issues automatically
./vendor/bin/pint

# Check for type errors
php -d memory_limit=2G vendor/bin/phpstan analyse --no-progress

# Run tests locally
php artisan test

# Now push — CI will agree
git push origin feature/my-feature

After a few weeks this becomes muscle memory. The quality gate stops being something that catches you and starts being something you pre-empt.

In the next post we cover secrets and environment variables — the right way to handle API keys, database passwords, and OAuth keys in CI without ever putting sensitive values in your YAML files.

Frequently Asked Questions

How do I use PHPStan on a legacy Laravel codebase with thousands of errors?
Generate a baseline with --generate-baseline. Existing errors are recorded and silenced, while any new error still fails CI. Re-generate the baseline as you fix old errors.
What is the difference between pint and pint --test?
pint auto-fixes style issues in place; pint --test only checks and exits non-zero if any file has style drift, which is what you want in CI.
D
Dinesh Wijethunga

Senior Full Stack Developer · Building SaaS products & teaching Laravel/React · 10+ years experience · Founder of Orion360 · Based in Dubai, UAE.

Was this post helpful?

Reviews & Ratings

Sign in to leave a review.

Comments(0)

Guest comments are held for moderation.

You might also like