---
title: Automating Code Quality in WordPress Plugins: PHPStan, Rector & GitHub Actions
description: Build an enterprise CI/CD pipeline for WordPress plugins. Configure PHPStan Level 8, automated Rector refactoring, and strict GitHub Actions PR gates.
url: https://wpstack.online/2026/09/26/automating-code-quality-phpstan-rector-actions
date_modified: 2026-09-12
author: Aditya Bhimrajka
language: en_US
---

Automated code quality is most useful when each tool has one clear job. PHPStan should find type and control-flow defects. Rector should propose deterministic refactors. Tests should protect behavior. Coding standards should catch consistency and WordPress-specific mistakes. GitHub Actions should run the same commands developers run locally.

The goal is not to advertise a maximum rule level. It is to create a small, dependable gate that a plugin team can keep green.

## Start with Composer scripts

Put the commands behind stable Composer scripts before writing CI configuration. This keeps the workflow portable and makes failures easy to reproduce.

```
{
  "scripts": {
    "analyse": "phpstan analyse --no-progress",
    "refactor:check": "rector process --dry-run",
    "test": "phpunit",
    "quality": [
      "@analyse",
      "@refactor:check",
      "@test"
    ]
  }
}
```

Pin development dependencies through `composer.lock`. CI should use `composer install`, not update packages to different versions on every run.

## Adopt PHPStan incrementally

PHPStan currently provides cumulative levels from 0 through 10, plus `max`. A legacy plugin does not become safer merely because its configuration says “max” while thousands of errors are ignored. Start at the highest level the codebase can sustain, then ratchet upward.

```
# phpstan.neon.dist
parameters:
    level: 6
    paths:
        - src
        - tests
    tmpDir: .cache/phpstan

```

WordPress exposes many dynamic functions and hook-driven values. Use maintained stubs or an appropriate WordPress extension so PHPStan understands platform symbols. Keep handwritten stubs narrow and verify them against supported WordPress versions.

A baseline can make an existing project adopt stricter analysis without blocking all work. Treat it as a debt register: prevent new entries, remove existing ones when touched, and review baseline changes in pull requests. Do not regenerate it automatically in CI.

## Use types to express WordPress boundaries

Static analysis becomes valuable when inputs stop being anonymous arrays. Validate REST data, shortcode attributes, options, and hook arguments at the boundary, then pass typed values into the domain layer.

```
final class Settings {
    public function __construct(
        public readonly bool $enabled,
        public readonly int $batchSize,
    ) {}
}

function settings_from_option(mixed $value): Settings {
    $data = is_array($value) ? $value : [];

    return new Settings(
        enabled: !empty($data['enabled']),
        batchSize: max(1, min(500, absint($data['batch_size'] ?? 50))),
    );
}
```

Do not silence a real uncertainty with a broad `@var` annotation. Narrow it with validation or teach the analyzer the precise return type.

## Make Rector reviewable

Rector changes code; that makes it different from a linter. Configure explicit paths and small rule sets, then run `vendor/bin/rector process --dry-run` in CI. Apply changes locally and review the diff.

```
<?php
// rector.php
use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths([
        __DIR__ . '/src',
        __DIR__ . '/tests',
    ])
    ->withPreparedSets(
        deadCode: true,
        codeQuality: true,
    );
```

Introduce one coherent set at a time. A refactor that produces hundreds of unrelated changes is harder to validate and merge. Generated files, bundled vendor code, fixtures, and compatibility shims may need explicit exclusions.

## Run a safe GitHub Actions matrix

Test the minimum and current PHP versions your plugin claims to support. Use least-privilege token permissions and pin third-party actions to trusted versions or commit SHAs according to your organization’s policy.

```
name: quality

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  php:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        php: ['8.1', '8.4']
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          coverage: none
      - run: composer install --no-interaction --prefer-dist
      - run: composer quality

```

If the plugin supports multiple WordPress or WooCommerce versions, add integration jobs for meaningful compatibility boundaries rather than creating a combinatorial matrix. Static analysis does not replace an install/activate smoke test against a real WordPress database.

## Add the missing WordPress checks

- Run WordPress Coding Standards on first-party PHP.
- Scan the declared minimum PHP version for incompatible syntax and APIs.
- Build the distribution archive and verify it excludes tests, secrets, caches, and development dependencies.
- Install and activate the built archive in a clean WordPress environment.
- Run security-focused tests for nonces, capabilities, sanitization, escaping, and prepared SQL.

## Keep CI trustworthy

Do not expose repository secrets to untrusted pull-request code. Avoid executing modified build scripts in a privileged workflow. Cache dependencies only when the cache policy is safe for the trigger, and never store credentials in a cache. Give release jobs stronger permissions only when they actually publish an artifact.

Require the quality workflow on protected branches, but keep it fast enough that developers do not work around it. Split slow integration tests from fast static checks when needed, then preserve both as merge requirements according to risk.

## A maintainable rollout

1. Make existing tests deterministic.
2. Add PHPStan at a sustainable level and block new errors.
3. Add Rector in dry-run mode with one small rule set.
4. Add WordPress standards and compatibility checks.
5. Test the built plugin across supported PHP and platform boundaries.
6. Raise strictness one step at a time.

The best quality pipeline is not the one with the most badges. It is the one that catches regressions early, produces actionable failures, and stays aligned with the versions the plugin actually supports.

Refer to the official [PHPStan rule levels](https://phpstan.org/user-guide/rule-levels) and [baseline guide](https://phpstan.org/user-guide/baseline), the [Rector documentation](https://getrector.com/documentation), and the GitHub [workflow syntax](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax) and [cache security guidance](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching).
