Practical guide
How to set up CI checks for a PHP project
Run the same checks on every pull request in a clean, reproducible, and secure environment.
In short
CI is an automated merge condition
Prepare
Align local and CI commands
First expose every check as a Composer or Make target that developers can also run.
- Committed composer.json, composer.lock, and configuration for PHPUnit, PHPStan, and CodeSniffer.
- One explicit supported PHP version and the required extension list.
- An isolated test database or other service containers with versions close to production.
- Protected-branch rules, least-privilege CI tokens, and an owner for failing checks.
Steps 1 to 3
Compose the pipeline from small checks
Return fast failures first and run expensive independent tests in parallel.
1. Install exactly locked dependencies
- Trigger the workflow on pull_request and pushes to the main branch. Cancel older runs of the same branch with a concurrency group.
- Check out the exact commit, configure the supported PHP and extensions, and run composer validate --strict.
- Install with composer install, never update. Cache Composer downloads by OS, PHP, and composer.lock hash, not an unverified vendor directory.
- The first job also runs PHP syntax or project validation so trivial errors stop the pipeline quickly.
composer validate --strict && composer install --no-interaction --prefer-dist Official Composer validate documentation 2. Run style, analysis, and tests separately
- CodeSniffer checks the committed standard without automatic fixes; PHPStan uses the same paths, level, and baseline as local runs.
- Split PHPUnit into a fast unit suite and an integration suite with a database service. Run real migrations on an empty schema before tests.
- Independent jobs run in parallel with individual timeouts. Choose fail-fast deliberately so useful diagnostics do not vanish after one failure.
- Maintain one aggregate required check or stable job names; a workflow rename must not accidentally bypass merge protection.
vendor/bin/phpcs && vendor/bin/phpstan analyse && vendor/bin/phpunit Official GitHub Actions workflow syntax 3. Publish diagnostics and restrict permissions
- Store JUnit reports, coverage, and relevant logs as artifacts even on failure, but exclude passwords and personal data.
- Default permissions to contents: read. Do not expose secrets to untrusted fork code or interpolate its values directly into shell scripts.
- Pin third-party actions to a reviewed version or commit SHA and update them regularly. Treat cache as untrusted input.
- Measure queue and runtime. Optimize or split a slow suite while retaining the same required quality for every change.
permissions: { contents: read } Official GitHub Actions security reference Step 4
Verify the pipeline with intentional failures
Every required gate must truly block a pull request and show its cause.
-
Break formatting, a type, and a test
The matching jobs fail independently with readable logs. After reverting, the entire commit is green.
composer ci -
Start with a clean database
Migrations and integration tests need no hand-made local tables or data from a previous run.
-
Check merge rules
A pull request cannot merge without every required check, and a fork workflow has no production secrets or write token.
Troubleshooting
Common problems
CI and local runs report different errors
Align PHP, extensions, locale, time zone, composer.lock, and commands in one Composer or Make target.
php -v && composer check-platform-reqs The pipeline is inconsistent after dependency changes
The cache key must include the composer.lock hash. Restoring cache never replaces composer install and validation.
Integration tests are flaky
Give every job an isolated database, deterministic fixtures, a service health check, and no order dependency.
A fork pull request can see a secret token
Restrict permissions, withhold secrets from untrusted triggers, and never run foreign code in privileged pull_request_target.
Done
Every commit passes the same quality gate.
Composer, CodeSniffer, PHPStan, and PHPUnit run reproducibly, results are required for merge, and the workflow uses minimum permissions.