Analyze Your Code Automatically with PHPSTAN

In WordPress projects, maintaining clean and error-free code is essential to ensure quality, security, and scalability. Tools like PHPStan (static analysis) and PHPLint (syntax validation) easily integrate into your automated testing, helping you detect issues before they reach production.

In this post, you will learn how to add both tools to your WordPress tests step by step.


Why use PHPStan?

  • PHPStan: analyzes your code without executing it, detecting type errors, calls to non-existent methods, and bad practices.
  • Both tools improve the quality of the code and facilitate collaboration in teams of WordPress development.

Installation with Composer

Make sure you have Composer installed in your development environment. Then add the dependencies:

composer require --dev phpstan/phpstan wp-coding-standards/wpcs szepeviktor/phpstan-wordpress phpstan/extension-installer

This will install both tools inside your folder vendor/.


PHPStan Configuration

  1. Create the file phpstan.neon.dist in the root of the project (include WooCommerce):
parameters:
    level: 1
    paths:
        - includes
    bootstrapFiles:
        - tests/phpstan-bootstrap.php
    excludePaths:
        - vendor/
        - node_modules (?)

Example phpstan

<?php
/**
 * PHPStan Bootstrap File
 * 
 * This file defines constants and functions that PHPStan needs to understand
 * but are not available during static analysis.
 */

// Define plugin constants that are used throughout the codebase
if (!defined('CONHOLD_PLUGIN_URL')) {
    define('CONHOLD_PLUGIN_URL', 'http://localhost/wp-content/plugins/connect-ecommerce/');
}

if (!defined('CONHOLD_VERSION')) {
    define('CONHOLD_VERSION', '1.0.0');
}

if (!defined('CONHOLD_FILE')) {
    define('CONHOLD_FILE', __FILE__);
}

// Define WordPress constants that might be missing
if (!defined('DOING_AJAX')) {
    define('DOING_AJAX', false);
}

if (!defined('WP_DEBUG')) {
    define('WP_DEBUG', false);
}

if (!defined('ABSPATH')) {
    define('ABSPATH', '/path/to/wordpress/');
}

// Mock WordPress functions that PHPStan can't find
if (!function_exists('wp_doing_ajax')) {
    function wp_doing_ajax() {
        return defined('DOING_AJAX') && DOING_AJAX;
    }
}

if (!function_exists('CONHOLD_get_options')) {
    function CONHOLD_get_options() {
        return [];
    }
}


// Mock Action Scheduler function
if (!function_exists('as_schedule_recurring_action')) {
    function as_schedule_recurring_action($timestamp, $interval_in_seconds, $hook, $args = [], $group = '') {
        return true;
    }
}

// Mock WP_CLI class
if (!class_exists('WP_CLI')) {
	class WP_CLI {
			public static function line($message) {
					echo $message . "\n";
			}
			public static function add_command($command, $class) {
					return true;
			}
	}
}

Configure composer.json:

"scripts": {
	"format": "phpcbf --standard=phpcs.xml.dist",
	"lint": "phpcs --standard=phpcs.xml.dist",
	"phpstan": "phpstan analyse --memory-limit=2048M"
}
  1. Run the analysis:
composer phpstan

Configuration with WooCommerce

composer require --dev php-stubs/wordpress-stubs php-stubs/woocommerce-stubs

And add the include in the configuration file

parameters:
    bootstrapFiles:
        - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php
        - vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php
        #- vendor/php-stubs/woocommerce-stubs/woocommerce-packages-stubs.php

PHPLint Configuration

  1. Create a file .phplint.yml:
path: .
jobs: 10
extensions:
  - php
  1. Run the analysis:
vendor/bin/phplint

Integration with PHPUnit and GitHub Actions

You can add both checks in your CI/CD pipelines so that every commit or pull request goes through these validations.

And add the file with your own Coding Standards rules:

<?xml version="1.0"?>
<ruleset name="Coding Standards for Internal Scanner Tool">
	<description>Description of Plugin.</description>

	<exclude-pattern>*/vendor/*</exclude-pattern>
	<exclude-pattern>reports/*</exclude-pattern>
	<exclude-pattern>php/tests/*</exclude-pattern>
	<exclude-pattern>php/prt_phpunit/*</exclude-pattern>

	<arg value="ps"/>
	<arg name="extensions" value="php"/>

	<file>./plugin.php</file>
	<file>./includes</file>
	<file>./templates</file>

	<rule ref="WordPress">
		<exclude name="Universal.Arrays.DisallowShortArraySyntax"/>
		<exclude name="WordPress.DB" />
		<exclude name="WordPress.Files.FileName"/>
		<exclude name="WordPress.NamingConventions.ValidFunctionName" />
	</rule>

	<rule ref="WordPress.WP.I18n">
		<properties>
			<property name="text_domain" type="array" value="plugin-name" />
		</properties>
	</rule>

	<rule ref="WordPress.Utils.I18nTextDomainFixer">
		<properties>
			<property name="old_text_domain" type="array">
				<element value="" />
			</property>
			<property name="new_text_domain" value="plugin-name" />
		</properties>
	</rule>
</ruleset>

This way, when running composer format or composer lint, PHPUnit, PHPStan, and PHPLint will run together.

Add in .github/workflows/php-lint.yml

name: PHP Code Linting

on:
  push:
    branches:
      - trunk
      - 'release/**'
    # Only run if PHP-related files changed.
    paths:
      - '.github/workflows/php-lint.yml'
      - '**.php'
      - 'phpcs.xml.dist'
      - 'composer.json'
      - 'composer.lock'
  pull_request:
    branches:
      - trunk
      - 'release/**'
      - 'feature/**'
    # Only run if PHP-related files changed.
    paths:
      - '.github/workflows/php-lint.yml'
      - '**.php'
      - 'phpcs.xml.dist'
      - 'composer.json'
      - 'composer.lock'
    types:
      - opened
      - reopened
      - synchronize

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/trunk' }}
jobs:
  php-lint:
    name: PHP
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4

      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.0'

      - name: Validate Composer configuration
        run: composer validate

      - name: Install PHP dependencies
        uses: ramsey/composer-install@a2636af0004d1c0499ffca16ac0b4cc94df70565
        with:
          composer-options: '--prefer-dist'

      - name: PHP Lint
        run: composer lint

      - name: PHP PHPStan
        run: composer phpstan

Conclusion

Integrating PHPStan and PHPLint into your WordPress TESTs is an easy way to elevate the quality of development, detect errors early, and improve the robustness of your plugins and themes.

If you want to take your project to the next level, start by adding these tools to your workflow. 🚀👉 Do you want me to prepare this article in complete markdown format ready to publish on WordPress (with headings h2, h3, code blocks, and links to the tools), or would you prefer I leave it in plain text for you to adapt?

References: