PhpStorm and CI/CD: How Local Checks and Pipeline Checks Fit Together
AI generated
IDE
{ }
PhpStorm · CI/CD · GitHub Actions · Pre-Commit · Quality Gates
PhpStorm and CI/CD:
how local and pipeline checks fit together

When PHPStan is green locally but red in the CI pipeline, it comes down to a configuration mismatch. The problem is solvable: align pre-commit hooks, PhpStorm quality gates and the CI pipeline so that what runs locally checks exactly what the pipeline checks, no push that fails right out of the gate.

20 min read Pre-Commit · GitHub Actions · GitLab CI · PHPStan · PHPCS · PHPUnit PhpStorm 2024+ · PHP 8.4 · Docker

1. The CI parity problem: why local doesn't equal pipeline

The most common reason for the gap between local and pipeline checks is environment differences: the local PHP has a different version than the CI container, Composer packages have a different lock file state locally, or PHPStan runs with different configuration paths or a different level. In Docker-based setups this problem is almost fully solvable, because you can use the same container environment locally as in the pipeline. Without Docker, the gap is structurally harder to close.

A second problem: what shows up as an inspection in PhpStorm and what the CI pipeline checks are often not the same thing. PhpStorm inspections are incremental and IDE-specific. PHPStan and PHPCS in the pipeline run with project-specific configuration files that the IDE doesn't automatically load. The goal, therefore, isn't to replace PhpStorm inspections with CI, but to align both levels and insert pre-commit hooks as a third level in between.

The three-level model for PHP quality assurance: (1) PhpStorm inspections while typing, fast, incremental, incomplete. (2) Pre-commit hooks at commit time, complete for the changed files, blocking. (3) The CI pipeline at push time, complete for the entire project, the authoritative instance. Each level has its own job; none replaces another.

2. Pre-commit hooks: catching errors before the push

Git hooks in the .git/hooks/ directory are the classic tool for pre-commit checks, but they have a drawback: they are not committed to the repository and must be set up manually by every developer. The pre-commit framework (Python based) or a simple composer scripts solution are better alternatives for PHP projects.

A lean solution without an external framework: a Composer script pre-commit that runs PHPCS on the changed files and PHPStan on the project, plus a prepare-commit-msg hook that calls this script. The script is committed to the repository, and the hook reference is set up automatically via composer install. In composer.json, under scripts.post-install-cmd, the hook link is set.


// composer.json - Pre-Commit Integration
{
    "scripts": {
        // Quality gate script
        "check": [
            "@phpcs",
            "@phpstan",
            "@phpunit-fast"
        ],
        "phpcs": "vendor/bin/phpcs --standard=phpcs.xml",
        "phpcbf": "vendor/bin/phpcbf --standard=phpcs.xml",
        "phpstan": "vendor/bin/phpstan analyse --no-progress --memory-limit=1G",
        "phpunit-fast": "vendor/bin/phpunit --testsuite=unit --no-coverage",
        // Hook setup after composer install/update
        "post-install-cmd": [
            "test -d .git && cp scripts/pre-commit.sh .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit || true"
        ],
        "post-update-cmd": [
            "@post-install-cmd"
        ]
    },
    "scripts-descriptions": {
        "check": "Runs all quality gate checks (PHPCS + PHPStan + PHPUnit)",
        "phpcs": "PHP CodeSniffer: check code style",
        "phpcbf": "PHP Code Beautifier: automatically fix code style",
        "phpstan": "PHPStan static analysis",
        "phpunit-fast": "PHPUnit unit tests (without coverage)"
    }
}

PhpStorm can run Composer scripts directly from the Composer tool window. The tool window opens via View → Tool Windows → Composer and lists all defined scripts. Clicking check runs all quality gate checks. For even faster feedback, you can set up the check script as a run configuration and assign it a shortcut.

3. Bundling local quality gates in PhpStorm

A local quality gate is a defined checkpoint before the commit that ensures code meets certain quality requirements. In PhpStorm, a quality gate can be represented as a compound run configuration: a configuration that runs PHPCS, PHPStan and PHPUnit one after another and stops on failure. The compound stops at the first failed step, so you immediately know which tool found a problem.

For everyday work, a tiered strategy is recommended: a fast quality gate (PHPCS + PHPStan on the current file, under 5 seconds) on save or on a shortcut, and a full quality gate (everything on the whole project) before the commit. The fast gate gives immediate feedback, the full gate gives confidence before the push.

4. GitHub Actions: PHP quality checks in the pipeline

GitHub Actions workflows for PHP projects can be edited directly in PhpStorm, the IDE recognizes .github/workflows/*.yml files and offers autocompletion for actions syntax. For maximum parity with the local Docker setup, it's recommended to use the same PHP container image in the pipeline as locally.

A minimal but complete GitHub Actions workflow for PHP quality checks: pin the PHP version, use a Composer cache, run composer install, then run PHPCS, PHPStan and PHPUnit sequentially. Each step has a clear name and the result shows up in the GitHub UI as an individual check status. If PHPStan fails, you know immediately which tool caught the problem.


# .github/workflows/quality.yml
name: PHP Quality Checks

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  quality:
    runs-on: ubuntu-latest
    container:
      # Same container as locally (Mark Shust image)
      image: markoshust/magento-php:8.4-fpm-0

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Cache Composer packages
        uses: actions/cache@v4
        with:
          path: vendor
          key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-composer-

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

      - name: PHP_CodeSniffer
        run: composer phpcs -- --report=checkstyle | cs2pr

      - name: PHPStan
        run: composer phpstan

      - name: PHPUnit
        run: composer phpunit-fast
        env:
          MAGENTO_UNIT_TEST: "1"

The tool cs2pr (Code Style to Pull Request) converts PHPCS output into GitHub annotations that appear directly as comments in the pull request. That way, during code review, the developer sees exactly which line has a PHPCS violation without having to read the raw output. Installation: composer require --dev staabm/annotate-pull-request-from-checkstyle.

5. GitLab CI: identical checks with Docker images

GitLab CI works analogously to GitHub Actions but uses a different YAML syntax and offers native Docker registry integration. The advantage with GitLab: you can host your own Docker images with pre-installed PHP dependencies in your own registry, which significantly reduces pipeline startup times.

In .gitlab-ci.yml you define one pipeline job per quality tool. With GitLab CI artifacts, you can attach PHPUnit coverage reports and PHPStan output as downloadable artifacts of the pipeline. This allows follow-up investigation when a pipeline run fails, without rerunning the check locally. PhpStorm Ultimate has native GitLab CI integration that shows pipeline status directly in the IDE.

6. Running PHPUnit identically locally and in CI

PHPUnit configuration must be identical between local execution and CI. The most important source of divergence: environment variables. Magento unit tests need certain constants and paths that are available locally in the container but must be set in CI. With a phpunit.xml.dist in the repository and a local phpunit.xml (which is in .gitignore), you can cover both scenarios.

PhpStorm reads phpunit.xml.dist automatically for run configurations. The bootstrap file loads Magento-specific constants and the autoloader. In the CI pipeline, the PHPUnit call references the same configuration file, which ensures identical behavior. Code coverage should run locally on demand and in CI on every PR build, but never in pre-commit hooks, since they are too slow for that.


<?xml version="1.0" encoding="UTF-8"?>
<!-- phpunit.xml.dist - Repository configuration for CI and local execution -->
<phpunit
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
    bootstrap="src/dev/tests/unit/framework/bootstrap.php"
    colors="true"
    beStrictAboutOutputDuringTests="true"
    beStrictAboutTestsThatDoNotTestAnything="true"
>
    <testsuites>
        <testsuite name="unit">
            <directory>src/app/code/Mironsoft/*/Test/Unit</directory>
        </testsuite>
        <testsuite name="integration">
            <directory>src/app/code/Mironsoft/*/Test/Integration</directory>
        </testsuite>
    </testsuites>

    <source>
        <include>
            <directory>src/app/code/Mironsoft</directory>
        </include>
        <exclude>
            <directory>src/app/code/Mironsoft/*/Test</directory>
        </exclude>
    </source>

    <php>
        <!-- Environment variables for Magento tests -->
        <env name="MAGENTO_ROOT" value="src"/>
        <env name="TESTS_CLEANUP" value="enabled"/>
    </php>
</phpunit>

7. Caching in CI: Composer and the PHPStan baseline

Caching is the single most important lever for fast CI pipelines. Without caching, composer install runs from scratch on every commit and takes several minutes on large PHP projects. With a Composer cache based on the hash of composer.lock, install becomes a matter of unpacking already-cached packages, seconds instead of minutes.

PHPStan baseline files need to be available as an artifact between builds in CI. If the baseline is generated locally and committed to the repository, that's not a problem, it gets checked out along with the code. Important: don't add the baseline to .gitignore. PHPStan's internal caches (/tmp/phpstan/) can also be cached in CI, but they bring less benefit than the Composer cache.

8. Seeing CI feedback in PhpStorm

PhpStorm Ultimate has a GitHub integration that shows pull request status, check results and code review comments directly in the IDE. The GitHub plugin (built into PhpStorm Ultimate) shows all open PRs with their CI status in the Pull Requests tool window. If a CI job fails, it shows up immediately in PhpStorm, without needing to open the browser.

An analogous plugin exists for GitLab projects. In both cases it's worth configuring the CI notifications so that only failures, and not every successful build, trigger an IDE notification. Too many notifications lead to habituation and get ignored. Better: notifications only for red builds, and green builds as a passive status icon in the IDE footer.

9. Comparison: workflow with and without local CI parity

The difference in everyday workflow between a setup without and with local CI parity is substantial, not just in the number of failed pipeline runs, but also in the time developers spend debugging and switching context.

Aspect Without parity With local parity Time saved
Error detection After push in CI (minutes) At commit time (seconds) 5 to 15 min per error
Context switching IDE to GitHub to IDE Within the IDE Cognitive load significantly reduced
Pipeline costs Many unnecessary runs Only valid code pushed CI minutes reduced
Code review quality Style fixes in reviews Reviews focused on logic only Reviews more focused and shorter
Onboarding Every developer their own practice Unified quality gates New developers productive faster

The investment in local CI parity is primarily a configuration task: Docker setup, identical PHP versions, shared configuration files (phpstan.neon, phpcs.xml, phpunit.xml.dist) and pre-commit hooks. Once set up, the system runs without further effort and automatically keeps the quality level high.

10. Summary

The core problem between local and pipeline checks is environment inconsistency: different PHP versions, different configuration files, missing environment variables. The solution is consistent parity: Docker setup locally and in CI, shared configuration files in the repository, pre-commit hooks that run the same tools with the same configurations as the pipeline.

In this setup, PhpStorm plays the role of the early warning system: a fast, incremental feedback loop while typing. Pre-commit hooks are the safety net before the commit. The CI pipeline is the authoritative checking instance and only ever sees code that has already been checked locally. Composer scripts as a shared abstraction layer ensure that all three levels use the same commands.

PhpStorm & CI/CD, the essentials at a glance

Three-level model

PhpStorm (while typing) to pre-commit hook (at commit time) to CI pipeline (at push time). Each level has its own job, none replaces another.

Composer scripts

Shared abstraction layer: composer check calls PHPCS, PHPStan, PHPUnit. Identical locally, in the pre-commit hook and in CI.

GitHub Actions

Pin the PHP version, Composer cache, PHPCS with cs2pr for PR annotations, PHPStan, PHPUnit sequentially. Same Docker container as locally.

CI feedback in the IDE

PhpStorm Ultimate: GitHub/GitLab plugin shows PR status directly in the IDE. Notifications only on failures. Green builds as a passive status indicator.

Mironsoft

CI/CD integration, quality gates and DevOps for PHP teams

Quality gates that run identically locally and in CI?

We set up pre-commit hooks, PhpStorm quality gates and CI pipelines so that what runs locally checks exactly what the pipeline checks, no push that fails right out of the gate, no waiting on CI feedback for fixable code.

Pre-commit setup

Set up Composer scripts, Git hooks and PhpStorm run configurations for local quality gates

CI pipeline

GitHub Actions or GitLab CI with caching, PR annotations and the same container as locally

PHPUnit CI

Harmonize PHPUnit configuration for local and CI execution, coverage reports as CI artifacts

11. FAQ: PhpStorm and CI/CD

1PHPStan green locally, red in CI, why?
Different PHP version, Composer lock state, or phpstan.neon. Solution: Docker locally with an identical CI image. All configuration files in the repo.
2Setting up pre-commit hooks for PHP?
Define a Composer script check. Copy a bash hook to .git/hooks/pre-commit. Set it up automatically via post-install-cmd.
3What is cs2pr?
Converts PHPCS output into GitHub PR annotations. Violations appear directly as comments in the pull request, not only as pipeline logs.
4Coverage locally or in CI?
Coverage: not in pre-commit (too slow). Locally on demand in PhpStorm. In CI on every PR build as an artifact. Both make sense, but separately.
5Show CI status in PhpStorm?
PhpStorm Ultimate: GitHub/GitLab plugin. View to Tool Windows to Pull Requests. Only failures as notifications, green builds as footer status.
6Advantage of Composer scripts?
Portable abstraction layer: composer phpstan runs identically locally, in the hook and in CI. No duplicate configuration, no divergence.
7Caching Composer in GitHub Actions?
actions/cache@v4 with path: vendor and key: ...-${{ hashFiles('composer.lock') }}. Cache hit: seconds instead of minutes for install.
8Keep pre-commit hooks from becoming too slow?
Only fast checks in pre-commit: PHPCS + PHPStan on changed files. Slow checks (full PHPUnit, coverage) in CI. Target: under 30 seconds.
9Which configuration files into the repo?
phpstan.neon, phpstan-baseline.neon, phpcs.xml, phpunit.xml.dist, .github/workflows/*.yml, composer.lock, scripts/pre-commit.sh.
10Sync run configurations with CI?
Indirectly through Composer scripts: run config calls composer check, CI job too. Both are automatically in sync, same entry point.