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.
Originally published at dineshstack.com — read the full version with code samples and updates there.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.