Bundling PHPUnit, PHPStan, PHPCS and Security Checks in GitLab CI
AI generated
CI/CD
.yml
GitLab · PHP · Quality Assurance · Security
PHPUnit, PHPStan, PHPCS and Security Checks
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.

14 min read PHPUnit · PHPStan Level 8 · PHPCS · composer audit Magento 2.4 · PHP 8.4 · GitLab CI/CD

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.

11. FAQ: PHPUnit, PHPStan, PHPCS and Security Checks in GitLab CI

1Which QA tools matter most for Magento?
composer audit (security), PHPStan plus the bitExpert plugin (type errors), PHPCS plus the Magento standard (style), PHPUnit for custom modules.
2How do I prevent PHPStan false positives in Magento?
The bitExpert/phpstan-magento plugin understands factories, proxies and the object manager. Without this plugin, PHPStan is not usable in Magento.
3How do I run all test jobs in parallel?
All in the same stage, sharing a cache with policy: pull. GitLab starts every job in a stage at the same time on available runners.
4What does composer audit check?
Every installed package against the packagist.org security database. Known CVEs mean exit code 1, which fails the pipeline.
5Show PHPUnit results in a merge request?
--log-junit junit.xml plus artifacts.reports.junit: junit.xml in the job. GitLab shows the results directly in the merge request.
6Which PHPStan level should I start with?
Level 4 to 6 as a start, create a baseline for existing errors, and raise the level gradually. Do not start at level 8.
7Does PHPCS always have to block the pipeline?
No. Set allow_failure: true when introducing it, clean up with phpcbf, then switch to allow_failure: false.
8Which PHPCS standard for Magento?
magento/magento-coding-standard: PSR-12 plus Magento specific rules for DI, string interpolation and database queries.
9PHPStan with generated Magento code?
Make the generated/ folder available to the PHPStan job as an artifact from the build job. Factories and proxies are needed for correct type analysis.
10How often should security checks run?
On every merge request and daily via scheduled pipelines. New CVEs appear every day, so only a regular scan catches them in time.