Bundled in GitLab CI
Code that is never tested and never analyzed quietly accumulates errors. Bundling PHPUnit, PHPStan, PHPCS and composer audit as parallel GitLab CI jobs delivers feedback after every commit on tests, type safety, style and known security vulnerabilities, automatically and without manual effort.
Table of Contents
- 1. Why All Checks Belong in the Pipeline
- 2. Integrating PHPUnit into GitLab CI
- 3. PHPStan: Static Analysis as a Pipeline Job
- 4. PHPCS and Coding Standard Checks
- 5. Security Checks: composer audit and More
- 6. Running All Test Jobs in Parallel
- 7. Making Test Reports Visible in GitLab
- 8. QA Tools Compared
- 9. Magento Specific Considerations
- 10. Summary
- 11. FAQ
1. Why All Checks Belong in the Pipeline
Code quality tools that are only ever run locally, and only by conscientious developers, are not quality assurance, they are a hope. PHPUnit, PHPStan, PHPCS and composer audit only add real process protection when they run automatically on every commit and every merge request. Without that automation, drift is unavoidable: one developer skips PHPStan, another forgets the code style, and the result is a codebase that grows more inconsistent as pressure increases.
GitLab CI is the right place for this automation. The test jobs run in parallel, report results directly in GitLab merge requests as pass or fail markers, and prevent code that fails the checks from being merged into the main branch. This costs some configuration effort up front, but it pays off in any codebase that is actively developed for longer than a quarter. Especially in Magento projects with intensive module development, this safety net is not a luxury, it is basic operational hygiene.
2. Integrating PHPUnit into GitLab CI
Running PHPUnit in GitLab CI is conceptually simple, but Magento projects require a few precautions. Magento's test framework is built around integration tests that need a running database and a Redis instance. Unit tests, on the other hand, are fully isolated and run without external dependencies, making them the first and most important test type for custom modules and view models. The recommendation: run unit tests as their own job in the test stage, and reserve integration tests for staging or for dedicated services jobs.
GitLab lets you spin up MySQL and Redis as sidecar containers alongside a test job using services in the pipeline configuration. This also makes integration tests possible directly in the pipeline. Magento integration tests additionally require an install-config-files.php that configures the test database. For most projects this complexity is too high; unit tests with full isolation are the more pragmatic starting point.
# .gitlab-ci.yml: parallel quality assurance jobs in the test stage
test:phpunit:
stage: test
image: php:8.4-cli
variables:
COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.cache/composer"
cache:
key:
files: [composer.lock]
paths: [.cache/composer/, vendor/]
policy: pull
before_script:
- apt-get update -qq &&
apt-get install -y -qq git unzip libzip-dev
- docker-php-ext-install zip pdo_mysql
- composer install --no-dev --prefer-dist --no-interaction
# Install test dependencies separately
- composer require --dev
phpunit/phpunit:^11
--no-interaction --no-update
- composer update phpunit/phpunit --no-interaction
script:
- ./vendor/bin/phpunit
--testsuite Unit
--log-junit junit-unit.xml
--coverage-text
artifacts:
when: always
reports:
junit: junit-unit.xml
expire_in: 1 week
only:
- merge_requests
- main
- tags
test:phpstan:
stage: test
image: php:8.4-cli
cache:
key:
files: [composer.lock]
paths: [.cache/composer/, vendor/]
policy: pull
script:
# Run PHPStan at level 6 for Magento modules
- ./vendor/bin/phpstan analyse
app/code/
--level=6
--error-format=gitlab
--no-progress
> phpstan-report.json || true
- ./vendor/bin/phpstan analyse
app/code/
--level=6
--no-progress
artifacts:
when: always
paths: [phpstan-report.json]
expire_in: 1 week
only:
- merge_requests
- main
3. PHPStan: Static Analysis as a Pipeline Job
PHPStan finds errors that tests do not catch: wrong types, calls to methods that do not exist, undefined properties and logical inconsistencies that only blow up at runtime. For Magento projects, PHPStan at level 6 to 8 makes sense, depending on how mature the codebase is. Level 0 checks only obvious mistakes, while level 8 enforces complete type annotations. The pragmatic strategy: start at level 4, hide all existing errors behind a baseline, and then make sure every subsequent change introduces no new errors.
For Magento, the PHPStan plugin bitExpert/phpstan-magento is essential: it understands Magento specific patterns such as ObjectManager::get(), factory classes and proxy generation, and prevents thousands of false positives. Without this plugin, PHPStan produces so many false positives in any Magento project that the tool becomes unusable. The configuration belongs in a phpstan.neon file at the project root, which the GitLab CI job picks up automatically.
4. PHPCS and Coding Standard Checks
PHPCS (PHP_CodeSniffer) checks whether the code follows the defined coding standard. For Magento projects, the Magento2 standard from the magento/magento-coding-standard package is the right starting point. This standard covers PSR-12 plus Magento specific rules for comments, string interpolation, database queries and using dependency injection instead of the object manager. In GitLab CI, PHPCS runs as its own job in parallel with PHPStan and PHPUnit.
Important: PHPCS as a pipeline blocker makes sense, but only once the entire codebase is compliant. Anyone introducing PHPCS into an existing codebase is better off using --report=diff and phpcbf to auto-fix issues before switching on the pipeline check. The GitLab CI job for PHPCS can initially be configured as a warning with allow_failure: true and later switched to allow_failure: false once the codebase has been cleaned up.
5. Security Checks: composer audit and More
Security checks in the pipeline are not a nice-to-have, they are mandatory for any production e-commerce shop. The first and simplest check is composer audit: Composer 2.4+ ships this command built in, and it checks every installed package against the packagist.org security database. If a known CVE exists for an installed version, the command fails with exit code 1, which fails the pipeline job and notifies the team.
For deeper security analysis, local-php-security-checker is a good fit: it also queries the Symfony security database and outputs results in JSON format. GitLab Ultimate additionally offers built in SAST (Static Application Security Testing) and dependency scanning as ready-made template jobs. For projects without GitLab Ultimate, the combination of composer audit and a manually configured local-php-security-checker job is sufficient and free.
# Security and code style jobs running in parallel
test:phpcs:
stage: test
image: php:8.4-cli
cache:
key:
files: [composer.lock]
paths: [.cache/composer/, vendor/]
policy: pull
script:
# Run PHPCS with Magento2 standard on custom code only
- ./vendor/bin/phpcs
--standard=Magento2
--extensions=php
--ignore=*/vendor/*,*/generated/*
app/code/
allow_failure: false
only:
- merge_requests
- main
test:security:
stage: test
image: php:8.4-cli
cache:
key:
files: [composer.lock]
paths: [.cache/composer/]
policy: pull
script:
# Audit all installed packages for known CVEs
- composer audit --no-dev --format=json > security-audit.json
|| (cat security-audit.json && exit 1)
# Additional: check for abandoned packages
- composer outdated --no-dev --format=json |
php -r "
\$data = json_decode(file_get_contents('php://stdin'), true);
\$abandoned = array_filter(\$data['installed'] ?? [],
fn(\$p) => \$p['abandoned'] ?? false);
foreach(\$abandoned as \$p) {
echo 'ABANDONED: ' . \$p['name'] . PHP_EOL;
}"
artifacts:
when: always
paths: [security-audit.json]
expire_in: 1 month
only:
- merge_requests
- main
- schedules
6. Running All Test Jobs in Parallel
The decisive performance advantage of a properly configured test stage is parallel execution. PHPUnit, PHPStan, PHPCS and composer audit have no dependencies on one another, so they can all run at the same time on different GitLab runners. If each job takes two minutes on its own, the entire test stage still finishes in two minutes when run in parallel, not eight. That is the difference between a pipeline the team actually accepts and one that gets skipped for being too slow.
For parallel execution, all jobs need to be defined in the same stage and use independent caches. The build job in the previous stage produces the vendor/ directory as an artifact or via cache; every test job pulls this cache with policy: pull (read only, never write). That way the cache is never corrupted by concurrent writes, and all test jobs start at the same time as soon as the build stage completes.
7. Making Test Reports Visible in GitLab
GitLab has native support for JUnit XML reports: when a test job defines artifacts.reports.junit, the test results appear directly in the merge request as a clear table. Every failed test is shown with its error message, so the reviewer never has to dig through job logs. PHPUnit generates these reports with the --log-junit junit.xml flag; PHPStan can also output JUnit XML using --error-format=junit.
For code coverage reports, GitLab offers a coverage setting in the project settings: with a regex pattern such as /Lines:\s+(\d+\.\d+)%/, GitLab extracts the coverage value from the PHPUnit text report and displays it as a badge on the repository. That motivates teams to increase coverage over time, without needing an external tool such as Coveralls or Codecov.
8. QA Tools Compared
The four core quality tools each cover a different class of errors and complement one another. None of them can fully replace the others.
| Tool | Error Class | Magento Plugin Needed? | GitLab Report Format |
|---|---|---|---|
| PHPUnit | Runtime errors, logic bugs, regressions | No | JUnit XML |
| PHPStan | Type errors, undefined methods, logic errors | Yes (bitExpert/phpstan-magento) | JUnit XML / GitLab JSON |
| PHPCS | Style violations, standard deviations | Yes (magento/magento-coding-standard) | Text / Checkstyle XML |
| composer audit | Known CVEs in dependencies | No | JSON / Text |
| GitLab SAST (Ultimate) | Security vulnerabilities in your own code | No (built in) | GitLab Security Dashboard |
9. Magento Specific Considerations
Magento projects have specific requirements that standard PHP QA configurations do not cover. The most important is the distinction between custom code (app/code/) and Magento core plus third-party code (vendor/). PHPStan, PHPCS and PHPUnit should analyze custom code only. Misconfigured tools that also scan vendor/ produce thousands of irrelevant errors and make the output useless.
A second Magento specific aspect is the generated code dependency. Many Magento classes, including factories, proxies and interceptors, only exist after setup:di:compile runs. PHPStan needs these generated classes to perform correct type analysis. That means the PHPStan job either needs to receive the generated code as an artifact from the build job, or run its own setup:di:compile against a stub database. The simpler solution is to make the generated/ artifacts from the build stage available to the PHPStan job.
10. Summary
Bundling PHPUnit, PHPStan, PHPCS and composer audit in GitLab CI is not a single measure, it is a system. Each tool covers a different class of errors, and only in combination do they provide reliable quality assurance. Parallel execution in the test stage keeps the pipeline fast; JUnit reporting makes failures visible directly in the merge request; the Magento specific plugins prevent false positives. The result is a process that systematically prevents bad code instead of chasing it down in code review.
The pragmatic starting point for existing projects: enable composer audit first (immediate value, zero false alarms), then introduce PHPCS with allow_failure: true and clean up the codebase step by step, then add PHPStan with a baseline and gradually rising level, and finally PHPUnit for custom modules. This incremental approach keeps the team from being overwhelmed by a hundred errors and disabling the QA jobs.
PHP Quality Assurance in GitLab CI: The Essentials at a Glance
Run in parallel
PHPUnit, PHPStan, PHPCS and composer audit in the same test stage, all parallel, all independent, in under 3 minutes.
Check only your own code
Analyze app/code/, explicitly exclude vendor/ and generated/, otherwise you get thousands of false alarms.
Magento plugins
bitExpert/phpstan-magento and magento/magento-coding-standard are mandatory for meaningful analysis.
Roll it out step by step
composer audit first, then PHPCS, then PHPStan with a baseline, PHPUnit last. Never introduce every check at once.