
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.
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.
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.
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.
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.
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.
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.
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 and baseline guide, the Rector documentation, and the GitHub workflow syntax and cache security guidance.

Aditya Bhimrajka is a technology entrepreneur, product strategist, and software solutions expert with over a decade of experience building scalable web and mobile applications. His expertise spans SaaS, AI, cloud technologies, custom software development, and digital transformation. Passionate about solving real-world business challenges through technology, Aditya shares practical insights on WordPress, plugins, software development, startup growth, product strategy, and emerging technologies. At WPStack, he writes actionable, experience-driven content that helps developers, businesses, and website owners build secure, high-performing, and future-ready WordPress solutions.
Post a Comment