Coding Standards and Static Analysis
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This sub-section’s WordPress bibliography lists the reference material consulted while preparing these pages. |
WordPress has written coding standards for PHP, JavaScript, CSS and HTML, plus an accessibility standard, and a set of
tools that check most of them automatically. This page is for plugin and theme developers and for reviewers: what the
standards ask for, how the tools are installed and configured for the lumen-catalog plugin of the Lumen Press site,
what they found on a deliberately careless admin screen and how each finding was fixed, where they overlap with
Plugin Check, and how to run them in CI. PHPStan itself (levels, PHPDoc types, generics) is explained in
Static Analysis and Refactoring.
The page was written against WordPress 7.1.3 (released 2026-10-06, examples tested; 7.1.4, the security release of
2026-10-28, is the one to run in production), PHP 7.4 minimum and 8.3+ recommended, WPCS 3.4.1, @wordpress/scripts
37.0.0 and WP-CLI 2.12.0, and checked on 2026-10-28. Composer resolved PHP_CodeSniffer 3.13.6, PHPCompatibilityWP 2.1.8
(on PHPCompatibility 9.3.5), PHPStan 2.3.3, szepeviktor/phpstan-wordpress 2.0.4 and php-stubs/wordpress-stubs 7.1.2,
run in the composer:2 and php:8.4-cli images (php:7.4-cli for the syntax check) on a copy of the plugin; Plugin
Check 2.1.0 ran on a 7.1.3 site (wordpress:cli-2.12.0-php8.4, MariaDB 11.8). WordPress 7.2 (in beta, planned for
2026-12-08) changes nothing here.
The WordPress coding standards
The Coding Standards Handbook on developer.wordpress.org has five standards plus inline-documentation standards. They are mandatory for WordPress core; for plugins and themes they are "encouraged", but the parts about security, i18n and prefixes are what the Plugin Directory review and Plugin Check enforce anyway.
| Standard | Main rules | Checked by |
|---|---|---|
PHP |
Tabs for indentation, spaces inside parentheses, braces on the same line, |
WPCS ( |
PHP inline documentation |
A DocBlock on every file, class, function, hook and property, with |
WPCS ( |
JavaScript |
Tabs, spaces inside parentheses and brackets, semicolons, single quotes, |
|
CSS |
Tabs, one selector per line, lowercase properties and values, short lowercase hex colours ( |
|
HTML |
Valid markup, lowercase tags and attributes, quoted attribute values, a space before the slash of self-closing tags ( |
Not automated beyond what ESLint’s JSX rules and the W3C validator catch |
Accessibility |
WordPress commits to conform to WCAG 2.2 level A and AA; ATAG 2.0 and WAI-ARIA 1.1 are the other normative documents. |
Partly: |
The WordPress PHP style is not PSR-12 (see
PSR Standards and Coding Style). A plugin may use
PSR-12 instead, but then still runs the WPCS security, i18n and prefix sniffs. lumen-catalog follows the WordPress
style except for PSR-4 file names.
The toolchain
| Tool | Version used | Role |
|---|---|---|
PHP_CodeSniffer ( |
3.13.6 |
Runs the sniffs and fixes what is fixable. WPCS 3.4 requires 3.13.5 or later and does not support PHP_CodeSniffer 4 yet. |
|
3.4.1 |
The WordPress rulesets and sniffs (security, i18n, prefixes, deprecated functions, style), with PHPCSUtils 1.2.3, PHPCSExtra 1.5.1 and |
|
2.1.8 |
PHPCompatibility with exceptions for the polyfills WordPress ships. |
|
2.3.3, 2.0.4, 1.4.3 |
Type-based static analysis with WordPress stubs, return types of core functions and hook DocBlock validation. |
|
37.0.0 |
|
Installing the PHP tools with Composer
The development tools go into require-dev of the plugin’s composer.json, so the release build
(composer install --no-dev) ships none of them. The file below extends the one in
Plugin Fundamentals; the test packages are added in
Testing.
{
"name": "lumenpress/lumen-catalog",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"require": {
"php": ">=7.4"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^1.2",
"phpcompatibility/phpcompatibility-wp": "^2.1",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.3",
"szepeviktor/phpstan-wordpress": "^2.0",
"wp-coding-standards/wpcs": "^3.4.1"
},
"autoload": {
"psr-4": {
"Lumen\\Catalog\\": "src/"
}
},
"config": {
"platform": {
"php": "7.4.33"
},
"sort-packages": true,
"optimize-autoloader": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"phpstan/extension-installer": true
}
},
"scripts": {
"lint": "phpstan analyse --memory-limit=1G",
"format": "phpcbf",
"format:check": "phpcs"
}
}
-
The installer plugin registers the standards with PHP_CodeSniffer, so
config.allow-pluginsmust allow it;vendor/bin/phpcs -ithen listsPHPCompatibilityWP,WordPress,WordPress-Core,WordPress-DocsandWordPress-Extraamong the installed standards. -
config.platform.phpmakes Composer resolve versions that still run on PHP 7.4, the plugin’s floor; PHPStan also reads it as the PHP version to analyse for. -
The committed
composer.lockpins the exact versions listed above; the caret constraints only decide whatcomposer updatemay move to.^3.4.1keeps WPCS at or above 3.4.1, a security release: before it, theWordPress.WP.EnqueuedResourceParameterssniff (part ofWordPress-ExtraandWordPress) could execute commands on the machine scanning untrusted code, for example a CI job linting a pull request from a fork. -
The script names (
lintfor PHPStan,formatfor PHPCBF,format:checkfor PHPCS) are the ones the other PHP projects of this site use, so a CI pipeline runs the same commands everywhere.phpcbfexits with status 1 when it fixed something, socomposer formatlooks like a failure after a successful fix;composer format:checkconfirms.
phpcs.xml.dist for lumen-catalog
PHP_CodeSniffer looks for .phpcs.xml, phpcs.xml, .phpcs.xml.dist and phpcs.xml.dist, in that order, in the
current directory. The committed .dist file holds the project’s rules; a developer can override it locally with an
untracked phpcs.xml. The skeleton in Plugin Fundamentals starts with a minimal phpcs.xml.dist; the full ruleset
below replaces its contents (a leftover .phpcs.xml.dist next to it would win, as PHP_CodeSniffer reads the dotted
one first).
<?xml version="1.0"?>
<ruleset name="Lumen Catalog">
<description>WordPress Coding Standards for Lumen Catalog.</description>
<file>.</file>
<exclude-pattern>/vendor/*</exclude-pattern>
<exclude-pattern>/node_modules/*</exclude-pattern>
<exclude-pattern>/build/*</exclude-pattern>
<exclude-pattern>/artifacts/*</exclude-pattern>
<arg name="extensions" value="php"/>
<arg name="basepath" value="."/>
<arg name="parallel" value="8"/>
<arg value="sp"/>
<!-- Keep in sync with the Requires PHP and Requires at least headers. -->
<config name="testVersion" value="7.4-"/>
<config name="minimum_wp_version" value="6.9"/>
<rule ref="WordPress-Extra"/>
<rule ref="WordPress-Docs"/>
<rule ref="PHPCompatibilityWP"/>
<!-- From the full WordPress standard: unslash and sanitize every superglobal read. -->
<rule ref="WordPress.Security.ValidatedSanitizedInput"/>
<rule ref="WordPress.WP.I18n">
<properties>
<property name="text_domain" type="array">
<element value="lumen-catalog"/>
</property>
</properties>
</rule>
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
<properties>
<property name="prefixes" type="array">
<element value="lumen_catalog"/>
<element value="Lumen\Catalog"/>
</property>
</properties>
</rule>
<!-- Capabilities the plugin registers, so WordPress.WP.Capabilities does not report them as unknown. -->
<rule ref="WordPress.WP.Capabilities">
<properties>
<property name="custom_capabilities" type="array">
<element value="manage_lumen_catalog"/>
<element value="manage_lp_terms"/>
<element value="publish_lp_books"/>
</property>
</properties>
</rule>
<!-- PSR-4: class files are named after their class, not class-*.php. -->
<rule ref="WordPress.Files.FileName">
<exclude-pattern>/src/*</exclude-pattern>
<exclude-pattern>/tests/*</exclude-pattern>
</rule>
</ruleset>
| Element | Why |
|---|---|
|
Scan the plugin, never Composer or npm output, built assets or Playwright artifacts. |
|
Relative paths, eight files in parallel, sniff codes (needed for a precise ignore) and progress in the output. |
|
The lowest PHP version for PHPCompatibility: |
|
The |
|
|
|
Added on top of |
|
Every translation call must use |
|
Global functions, classes, constants, variables and hook names fired by the plugin must start with |
|
The |
|
PSR-4 names ( |
PHPCS and PHPCBF on a real class
A first draft of a Books > Stock screen, listing the stock movements of one book from the {$wpdb->prefix}lp_stock
table, was written in a hurry and booted from Plugin::boot():
<?php
namespace Lumen\Catalog\Admin;
class StockReport {
public static function boot() {
add_action( 'admin_menu', [ self::class, 'menu' ] );
}
public static function menu() {
add_submenu_page( 'edit.php?post_type=lp_book', __( 'Stock history', 'lumen' ), __( 'Stock', 'lumen-catalog' ), 'manage_lumen_catalog', 'lp-stock', [ self::class, 'render' ] );
}
public static function render() {
global $wpdb;
$book = $_GET['book'];
$rows = $wpdb->get_results( "SELECT changed_at, delta FROM {$wpdb->prefix}lp_stock WHERE book_id = $book" );
echo '<h1>' . __( 'Stock history', 'lumen-catalog' ) . '</h1><table>';
foreach ( $rows as $row ) {
echo "<tr><td>$row->changed_at</td><td>$row->delta</td></tr>";
}
echo '</table>';
if ( current_user_can( 'publish_lp_books' ) ) {
echo '<p>' . esc_html( self::total( $rows ) ) . '</p>';
}
/**
* Fires after the stock history table.
*/
do_action( 'stock_report_after', $book );
}
public static function total( $rows ) {
return array_sum( array_column( $rows, 'delta' ) );
}
public static function last_changed( int $book_id ): ?string {
$post = get_post( $book_id );
return $post?->post_modified;
}
}
From the plugin folder, the tools run in containers:
docker run --rm -v "$PWD":/app -w /app composer:2 composer install
docker run --rm -v "$PWD":/app -w /app php:8.4-cli vendor/bin/phpcs --report=summary
docker run --rm -v "$PWD":/app -w /app php:8.4-cli vendor/bin/phpcbf
docker run --rm -v "$PWD":/app -w /app php:8.4-cli vendor/bin/phpcs --report=emacs -q
Source: PHP_CodeSniffer wiki: Usage
The summary reported 47 errors and 1 warning in src/Admin/StockReport.php, 32 of them fixable. PHPCBF fixed those 32
(spaces to tabs, [] to array()) and exited with status 1; the second run listed the 16 that need a person:
src/Admin/StockReport.php:1:1: error - Missing file doc comment (Squiz.Commenting.FileComment.Missing)
src/Admin/StockReport.php:4:1: error - Missing doc comment for class StockReport (Squiz.Commenting.ClassComment.Missing)
src/Admin/StockReport.php:11:78: error - Mismatched text domain. Expected 'lumen-catalog' but got 'lumen'. (WordPress.WP.I18n.TextDomainMismatch)
src/Admin/StockReport.php:16:17: warning - Processing form data without nonce verification. (WordPress.Security.NonceVerification.Recommended)
src/Admin/StockReport.php:16:17: error - Detected usage of a possibly undefined superglobal array index: $_GET['book']. Check that the array index exists before using it. (WordPress.Security.ValidatedSanitizedInput.InputNotValidated)
src/Admin/StockReport.php:16:17: error - $_GET['book'] not unslashed before sanitization. Use wp_unslash() or similar (WordPress.Security.ValidatedSanitizedInput.MissingUnslash)
src/Admin/StockReport.php:16:17: error - Detected usage of a non-sanitized input variable: $_GET['book'] (WordPress.Security.ValidatedSanitizedInput.InputNotSanitized)
src/Admin/StockReport.php:17:37: error - Use placeholders and $wpdb->prepare(); found interpolated variable $book at "SELECT changed_at, delta FROM {$wpdb->prefix}lp_stock WHERE book_id = $book" (WordPress.DB.PreparedSQL.InterpolatedNotPrepared)
src/Admin/StockReport.php:18:23: error - All output should be run through an escaping function (see the Security sections in the WordPress Developer Handbooks), found '__'. (WordPress.Security.EscapeOutput.OutputNotEscaped)
src/Admin/StockReport.php:20:18: error - All output should be run through an escaping function (see the Security sections in the WordPress Developer Handbooks), found '"<tr><td>$row->changed_at</td><td>$row->delta</td></tr>"'. (WordPress.Security.EscapeOutput.OutputNotEscaped)
src/Admin/StockReport.php:29:20: error - Hook names invoked by a theme/plugin should start with the theme/plugin prefix. Found: "stock_report_after". (WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound)
The list leaves out five Squiz.Commenting.FunctionComment.Missing errors, one per method. The three
ValidatedSanitizedInput lines are the ones WordPress-Extra alone does not produce. The fixes, in the version below:
DocBlocks everywhere, the lumen-catalog domain, the input read with isset() and absint() (an integer sanitiser,
which also satisfies the unslash rule), the query through $wpdb->prepare() with %d, esc_html__() and
esc_html() on output, the hook renamed to lumen_catalog_stock_report_after, and total() declared as
total( array $rows ): int with a PHPDoc array shape. Escaping and preparing are
explained in Security for Developers.
/**
* Prints the stock movements of the book given in the "book" query argument.
*/
public static function render(): void {
global $wpdb;
// A read-only screen behind a capability check: no state changes, so no nonce.
$book_id = isset( $_GET['book'] ) ? absint( $_GET['book'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->prepare( "SELECT changed_at, delta FROM {$wpdb->prefix}lp_stock WHERE book_id = %d ORDER BY changed_at DESC", $book_id ),
ARRAY_A
);
echo '<h1>' . esc_html__( 'Stock history', 'lumen-catalog' ) . '</h1><table>';
foreach ( $rows as $row ) {
printf( '<tr><td>%s</td><td>%s</td></tr>', esc_html( $row['changed_at'] ), esc_html( $row['delta'] ) );
}
echo '</table>';
if ( current_user_can( 'publish_lp_books' ) ) {
echo '<p>' . esc_html( self::total( $rows ) ) . '</p>';
}
/**
* Fires after the stock history table.
*
* @param string $book_id ID of the book shown.
*/
do_action( 'lumen_catalog_stock_report_after', $book_id );
}
PHPCS then reported nothing. Each inline ignore names the sniff and says why, never a bare phpcs:ignore or a
phpcs:disable block. The DirectDatabaseQuery ignore is there for Plugin Check, which runs that sniff (see below).
PHPCompatibilityWP and the PHP floor
PHPCompatibility reports syntax and functions that do not exist on the versions in testVersion; the WP flavour does
not report functions WordPress polyfills (for example str_contains() since 5.9). The released PHPCompatibility 9.3.5
dates from 2019 and knows PHP up to 7.4, so with a 7.4 floor it has almost nothing left to find: the nullsafe operator
($post?->post_modified, PHP 8.0) in last_changed() above was not reported. PHPStan 2.3.3, which reads PHP 7.4.33
from config.platform.php, did not report it either. Only the PHP 7.4 interpreter did:
docker run --rm -v "$PWD":/app -w /app php:7.4-cli php -l src/Admin/StockReport.php
Source: PHP Manual: Command line options
Parse error: syntax error, unexpected '->' (T_OBJECT_OPERATOR) in src/Admin/StockReport.php on line 38
Errors parsing src/Admin/StockReport.php
Under PHP 7.4 that parse error would be fatal on every request, because Plugin::boot() loads the class on
plugins_loaded. A php -l pass on the oldest supported PHP (the CI job below has one) closes the gap
until PHPCompatibility 10, still in alpha, is released. The method was removed; get_post_modified_time() does the job.
PHPStan with phpstan-wordpress
PHPStan does not know WordPress functions unless it is given their signatures. szepeviktor/phpstan-wordpress loads
php-stubs/wordpress-stubs, adds return types that depend on arguments (get_post() with ARRAY_A, get_option()
defaults, is_wp_error() narrowing), defines core constants and checks the DocBlock above apply_filters() and
do_action() against the arguments passed. phpstan/extension-installer registers it, so the configuration names only
the paths and the level:
# szepeviktor/phpstan-wordpress is registered by phpstan/extension-installer.
parameters:
level: 6
paths:
- lumen-catalog.php
- uninstall.php
- src
excludePaths:
# render.php reads $attributes and $block, which WordPress injects.
- src/blocks
The file is phpstan.neon.dist (NEON syntax, highlighted as YAML here); PHPStan reads an untracked phpstan.neon
first, which leaves room for a local override. Level 6 is the house default: it requires parameter and return types
(native or PHPDoc) and value types of arrays, which is where WordPress code is usually weakest. The tests path and the
test stubs come with Testing. Block render.php files are excluded because their
$attributes, $content and $block variables are injected by WordPress; PHPCS still checks them.
docker run --rm -v "$PWD":/app -w /app php:8.4-cli vendor/bin/phpstan analyse --memory-limit=1G --no-progress --error-format=raw
Source: PHPStan: Getting Started
On the first draft, PHPStan reported five missing return and parameter types (missingType.return,
missingType.parameter), the same places the DocBlock sniffs found. On the version that already passed PHPCS it found
two real bugs that no sniff can see:
/app/src/Admin/StockReport.php:57:Parameter #1 $text of function esc_html expects string, int given.
/app/src/Admin/StockReport.php:65:@param string $book_id does not accept actual type of parameter: int<0, max>.
The first passes an integer to an escaping function, which works only because PHP coerces it; the fix formats the
number for the reader’s locale: esc_html( number_format_i18n( self::total( $rows ) ) ). The second is the hook
DocBlock check of phpstan-wordpress: the documented type of the action’s argument was wrong, and any developer hooking
lumen_catalog_stock_report_after would have trusted it; the DocBlock now says @param int $book_id. With both fixed,
PHPStan reported no errors.
Analysing only src/ gave Constant LUMEN_CATALOG_FILE not found.: the constant is defined in the main file, which
therefore belongs in paths (or in bootstrapFiles). At level 10, global $wpdb is mixed and every use of it is
an error (Cannot call method get_results() on mixed.).
Raising the level with a baseline
At level 9 the plugin had two errors: absint() receiving $_GET['book'] (mixed) and (int) applied to the
mixed result of get_option() in the installer. A baseline records such existing errors so that only new ones fail:
docker run --rm -v "$PWD":/app -w /app php:8.4-cli vendor/bin/phpstan analyse --memory-limit=1G --level=9 --generate-baseline
Source: PHPStan: The Baseline
It printed [OK] Baseline generated with 2 errors. and wrote phpstan-baseline.neon:
parameters:
ignoreErrors:
-
message: '#^Parameter \#1 \$maybeint of function absint expects array\|bool\|float\|int\|resource\|string\|null, mixed given\.$#'
identifier: argument.type
count: 1
path: src/Admin/StockReport.php
-
message: '#^Cannot cast mixed to int\.$#'
identifier: cast.int
count: 1
path: src/Installer.php
Source: PHPStan: The Baseline
Adding includes: [ phpstan-baseline.neon ] and level: 9 to phpstan.neon.dist made the run pass. A baselined
error that disappears is reported as a stale entry (reportUnmatchedIgnoredErrors). A baseline suits legacy code; the
house default stays at level 6 because higher levels mostly argue with the mixed returns of WordPress’s own API.
JavaScript and CSS: lint-js, lint-style and format
The block and admin code is checked by @wordpress/scripts, already a development dependency for the build (see
Block Development Environment).
The scripts in package.json:
{
"scripts": {
"lint:js": "wp-scripts lint-js src",
"lint:style": "wp-scripts lint-style \"src/**/*.{css,scss}\"",
"format": "wp-scripts format src"
}
}
The default ESLint configuration does not know the plugin’s text domain, so a wrong domain in JavaScript passes. An
eslint.config.cjs at the plugin root (ESLint 10 reads only flat configuration files; a leftover .eslintrc triggers a
warning and is ignored) extends the default and turns on the check:
const defaultConfig = require( '@wordpress/scripts/config/eslint.config.cjs' );
module.exports = [
...defaultConfig,
{
rules: {
'@wordpress/i18n-text-domain': [
'error',
{ allowedTextDomain: 'lumen-catalog' },
],
},
},
];
A careless edit.js for lumen/book-card (four-space indentation, an <img> without alt, a console.log, 'lumen'
as text domain, a translated string with a trailing space) first produced three import/no-extraneous-dependencies
errors: the @wordpress/* packages the code imports must be listed in dependencies, even though webpack leaves
them to WordPress. With those listed and the configuration above, npm run lint:js reported 18 problems: 12
prettier/prettier formatting errors and these six (path shortened):
src/blocks/book-card/edit.js
5:44 error 'setAttributes' is defined but never used no-unused-vars
8:9 error Unexpected console statement no-console
9:8 warning React Hook useEffect has a missing dependency: 'isbn'. Either include it or remove the dependency array react-hooks/exhaustive-deps
13:13 error img elements must have an alt prop, either with meaningful text, or an empty string for decorative images jsx-a11y/alt-text
14:17 error Invalid text domain 'lumen' @wordpress/i18n-text-domain
14:17 error Translations should not contain flanking whitespace (whitespace) @wordpress/i18n-no-flanking-whitespace
npm run format rewrote the indentation and spacing. The rest was fixed by hand: the unused setAttributes and the
useEffect logging went, the cover became <img src={ coverUrl } alt="" /> (decorative, since the title is
next to it), and the label became sprintf( __( 'ISBN: %s', 'lumen-catalog' ), isbn ) with a translators:
comment. lint:js then passed.
lint-style reported 11 errors on a three-rule style.scss written on single lines with #DDD (brace newlines,
@stylistic/color-hex-case, trailing semicolon, rule-empty-line-before), all fixed by --fix. It did not object
to the same selector appearing twice; the bundled configuration does not enable no-duplicate-selectors.
wp-scripts format always runs Prettier with --write: it has no check mode, and format --check src/tmp.js
wrote the file and formatted the whole project instead of the given path. CI therefore relies on the
prettier/prettier rule inside lint-js, or runs format and fails on git diff --exit-code, as below.
Overlap with Plugin Check
Plugin Check (see Publishing to the Plugin
Directory) bundles its own PHP_CodeSniffer and WPCS and ignores the plugin’s phpcs.xml.dist. Several of its
checks are single WPCS sniffs run with the full WordPress standard:
| Plugin Check check | What it runs | Covered by the lumen-catalog ruleset |
|---|---|---|
|
|
Yes ( |
|
|
Yes, with |
|
|
Partly: the guess can be stricter than the ruleset |
|
|
No; answered by |
|
Its own ruleset: prepared SQL, |
Mostly; |
Run on the fixed copy (wp plugin check lumen-catalog --checks=late_escaping,direct_db_queries,i18n_usage,prefixing,plugin_review_phpcs),
it printed "Success: Checks complete. No errors found." Two findings on the way show the differences:
-
With the
phpcs:ignorecomments removed,direct_db_queriesandplugin_review_phpcsreportedWordPress.Security.NonceVerification.Recommended,WordPress.DB.DirectDatabaseQuery.DirectQueryandWordPress.DB.DirectDatabaseQuery.NoCachingas warnings: Plugin Check honours the same inline ignores. -
An earlier ruleset also accepted
lumenas a prefix, so the$lumen_queryand$lumen_bookvariables of thelumen/latest-booksrender.phppassed PHPCS. Theprefixingcheck, which guesses the prefixes from the plugin’s code, did not acceptlumenand reported both asPrefixAllGlobals.NonPrefixedVariableFound. They are now$lumen_catalog_queryand$lumen_catalog_book, and the ruleset lists onlylumen_catalog.
The remaining full-run errors were the expected ones for a development folder: application_detected for
phpcs.xml.dist and phpstan.neon.dist, plugin_updater_detected for the private Update URI header and
no_plugin_readme. They disappear in the release build, which leaves tool configuration out through .distignore.
Plugin Check runs late, on a site, and has no PHPStan, PHPCompatibility, style or JavaScript checks.
Running everything in CI
The pipeline that builds and deploys the plugin is described in Deployment and CI/CD; its quality job runs the commands of this page. In GitHub Actions:
name: Quality
on:
pull_request:
push:
branches: [ main ]
jobs:
php:
runs-on: ubuntu-24.04
strategy:
matrix:
php: [ '7.4', '8.4' ]
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@2.40.0
with:
php-version: ${{ matrix.php }}
tools: composer:v2
coverage: none
- run: composer install --no-interaction --no-progress
- run: find . -name '*.php' -not -path './vendor/*' -not -path './node_modules/*' -print0 | xargs -0 -n1 php -l
- run: composer format:check
- run: composer lint -- --no-progress --error-format=github
js:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run lint:js
- run: npm run lint:style
- run: npm run format && git diff --exit-code
Source: GitHub: shivammathur/setup-php
actionlint 1.7.12 accepted the workflow and its PHP steps passed locally in Docker; it was not run on GitHub. The
PHP 7.4 leg catches syntax newer than the floor; --error-format=github turns PHPStan errors into annotations on
the diff. Pull requests from forks run the scanners on unreviewed code, so WPCS must stay at 3.4.1 or later and the
job gets no secrets. Themes (lumen, lumen-classic) use the same files with "type": "wordpress-theme", their own
text domain and prefix, and PHPStan paths functions.php, src and patterns; Theme Check is covered in
Theme Accessibility and Releasing.
Common errors and pitfalls
| Symptom | Cause and fix |
|---|---|
|
The installer plugin did not run: allow |
PHPCS ignores the new ruleset |
A |
Unsanitised |
|
PHPStan: |
The extension is not loaded: allow |
PHPStan: |
Exclude |
PHP 8 syntax reaches a PHP 7.4 site |
Neither PHPCompatibility 9.3.5 nor PHPStan reported the nullsafe operator; lint with |
|
|
Where older books and tutorials are outdated
-
Installing WPCS by cloning its repository and setting
installed_pathsby hand, or installing it globally, belongs to WPCS 2 and earlier; WPCS 3 (2023) supports only Composer and needs PHPCSUtils, PHPCSExtra and the installer plugin. TheWordPress-VIPruleset some tutorials reference was removed in WPCS 2.0. -
Old
// WPCS: XSS OKand// WPCS: input var okannotations stopped working in WPCS 3; onlyphpcs:ignorewith sniff codes is recognised. -
The JavaScript standard page and many books still describe JSHint and
.jshintrc. Block and admin code is linted with ESLint throughwp-scripts lint-js; with@wordpress/scripts37 (ESLint 10) the configuration is a flateslint.config.*file.