Coding standards with PHPCS and WPCS

Install PHP_CodeSniffer with the WordPress Coding Standards, configure it for the events plugin's prefix and text domain, and fix issues automatically.

  • Intermediate
  • 8 min read
  • Updated 17 Oct 2026

What you will learn

  • Run PHPCS with WPCS
  • Fix coding standard issues
  • Configure a project ruleset with your prefix and text domain
  • Suppress a warning responsibly

Before you start

WordPress has written coding standards for PHP, and a tool that enforces them: PHP_CodeSniffer (PHPCS) with the WordPress Coding Standards ruleset (WPCS). Formatting is the least of what it checks. WPCS flags unescaped output, missing nonce checks, unprepared SQL, untranslatable strings and unprefixed globals, which makes it a security reviewer that never gets tired. This lesson installs it in the events plugin, configures it, and fixes what it finds.

Installing with Composer

Install PHPCS and WPCS per project, as development dependencies, so every developer and the CI server use the same versions:

Shell
composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer require --dev wp-coding-standards/wpcs

WPCS pulls in PHP_CodeSniffer itself, plus a Composer plugin that registers the installed standards with PHPCS. Composer asks you to trust plugins before they run, which is what the first command does. Check that the standards are registered:

Shell
vendor/bin/phpcs -i

The list should include WordPress, WordPress-Core, WordPress-Docs and WordPress-Extra.

Which ruleset?

StandardCovers
WordPress-CoreFormatting and naming: tabs, spacing, braces, Yoda conditions, naming conventions
WordPress-DocsDocBlocks on functions, classes and files
WordPress-ExtraCore plus best practices: escaping output, nonce verification, sanitised input, prepared SQL, prefixing, i18n
WordPressAll three combined

For a plugin, WordPress (or WordPress-Extra if you do not want the documentation rules yet) is the right choice. The security sniffs live in Extra, and they are the main reason to run the tool.

The project ruleset

PHPCS reads its configuration from a ruleset file in the project root. Name it phpcs.xml.dist and commit it; a developer can override it locally with an untracked phpcs.xml.

HTML
<?xml version="1.0"?>
<ruleset name="MyPlugin">
    <description>Coding standards for My Plugin.</description>

    <!-- What to check. -->
    <file>.</file>
    <exclude-pattern>/vendor/*</exclude-pattern>
    <exclude-pattern>/node_modules/*</exclude-pattern>
    <exclude-pattern>/build/*</exclude-pattern>

    <!-- How to check. -->
    <arg name="extensions" value="php"/>
    <arg name="colors"/>
    <arg name="parallel" value="8"/>
    <arg value="sp"/>

    <!-- The oldest WordPress version the plugin supports. -->
    <config name="minimum_wp_version" value="6.5"/>

    <rule ref="WordPress"/>

    <!-- Every global name must start with one of these. -->
    <rule ref="WordPress.NamingConventions.PrefixAllGlobals">
        <properties>
            <property name="prefixes" type="array">
                <element value="myplugin"/>
                <element value="MyPlugin"/>
            </property>
        </properties>
    </rule>

    <!-- Every translatable string must use this text domain. -->
    <rule ref="WordPress.WP.I18n">
        <properties>
            <property name="text_domain" type="array">
                <element value="myplugin"/>
            </property>
        </properties>
    </rule>

    <!-- src/ follows PSR-4 file names, not class-*.php. -->
    <rule ref="WordPress.Files.FileName">
        <exclude-pattern>/src/*</exclude-pattern>
        <exclude-pattern>/tests/*</exclude-pattern>
    </rule>
</ruleset>

What each part does:

  • file and exclude-pattern check the whole plugin but skip third-party and generated code. You are not responsible for vendor/, and fixing it would be undone by the next install.
  • arg value="sp" shows progress (p) and the sniff code for each message (s). Sniff codes are what you need to configure or suppress a rule, so always show them.
  • minimum_wp_version tells sniffs that warn about deprecated functions which WordPress version you support.
  • prefixes lists your prefixes. The sniff then reports any global function, class, constant, hook name or global variable that does not start with one. Namespaced code counts as prefixed when the namespace starts with a listed prefix, so MyPlugin\Bookings\Repository passes.
  • text_domain reports any __(), esc_html_e() and similar call that uses another text domain, or none. A wrong text domain means the string is never translated, and it is an easy mistake to make when code is copied between projects.
  • WordPress.Files.FileName expects files named like class-repository.php. That conflicts with PSR-4 autoloading from lessons 16 and 17, which needs Repository.php. The exclusion switches the file name sniff off for src/ and tests/ only.

Running the checks

Shell
vendor/bin/phpcs

With no arguments, PHPCS uses the ruleset file. Check one file while you work:

Shell
vendor/bin/phpcs src/Bookings/Repository.php

Output looks like this:

Plain text
FILE: src/Admin/SettingsPage.php
----------------------------------------------------------------------
FOUND 2 ERRORS AFFECTING 2 LINES
----------------------------------------------------------------------
 42 | ERROR | [x] Expected 1 space after "!"; 0 found
    |       |     (WordPress.WhiteSpace.OperatorSpacing.NoSpaceAfter)
 57 | ERROR | [ ] All output should be run through an escaping function
    |       |     (see the Security sections in the WordPress Developer
    |       |     Handbooks), found '$settings'.
    |       |     (WordPress.Security.EscapeOutput.OutputNotEscaped)
----------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------

Errors marked [x] are fixable automatically. The rest need a human.

Fixing issues automatically

PHP Code Beautifier and Fixer (PHPCBF) ships with PHPCS and fixes everything marked [x]:

Shell
vendor/bin/phpcbf

It rewrites whitespace, indentation, array syntax, Yoda conditions and similar. Commit before running it on an existing codebase, then review the diff, so formatting changes are separate from real changes in your history.

Add both commands as Composer scripts, so nobody needs to remember the paths:

JSON
{
    "scripts": {
        "lint": "phpcs",
        "lint:fix": "phpcbf"
    }
}

Now composer lint and composer lint:fix work on every machine.

Fixing issues by hand

The remaining issues are often the important ones. Fix the cause rather than silencing the message:

PHP
<?php
// Before: flagged by WordPress.Security.EscapeOutput.OutputNotEscaped
echo $settings['max_guests'];

// After: escaped for an HTML text context.
echo esc_html( (string) $settings['max_guests'] );
PHP
<?php
// Before: flagged by WordPress.Security.NonceVerification.Missing
// and WordPress.Security.ValidatedSanitizedInput.
$event_id = $_POST['event_id'];

// After: nonce checked, input unslashed and sanitised.
check_admin_referer( 'myplugin_cancel_booking' );
$event_id = isset( $_POST['event_id'] ) ? absint( wp_unslash( $_POST['event_id'] ) ) : 0;

Suppressing a warning responsibly

Sometimes the code is correct and the sniff cannot tell. uninstall.php is a clear case: it drops the plugin's own table, so there is no core API to use instead and nothing to cache. WPCS still reports a direct query and a schema change. Suppress the specific sniffs, on the specific line, and say why:

PHP
<?php
// myplugin/uninstall.php

defined( 'WP_UNINSTALL_PLUGIN' ) || exit;

global $wpdb;

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- Removing the plugin's own table on uninstall.
$wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $wpdb->prefix . 'myplugin_bookings' ) );

The database sniffs look for the $wpdb variable, so they may say nothing about queries made through a property such as $this->wpdb in the repository. Do not read silence as approval: that class still needs review, which is one more reason to keep all SQL in one place.

Rules for ignores:

  • Always name the sniff code. A bare // phpcs:ignore silences every rule on the line, including security rules you did not mean to switch off.
  • Always add a reason after --. Reviewers, and you in six months, need to know why the rule does not apply.
  • Prefer phpcs:ignore (next line) over phpcs:disable and phpcs:enable around a block, which is easy to leave too wide.
  • Never ignore WordPress.DB.PreparedSQL or EscapeOutput to make a warning go away. If the sniff is wrong, the comment must explain exactly why the value is safe.

If a rule genuinely does not suit the whole project, change it once in phpcs.xml.dist, with a comment, rather than scattering ignores.

Adopting it on an existing plugin

Running WPCS on a plugin that has never used it can produce thousands of messages. Do not try to fix them in one go:

  1. Run phpcbf and commit the formatting changes on their own.
  2. Fix all WordPress.Security and WordPress.DB.PreparedSQL errors next. They are the ones that matter for users.
  3. Work through the rest file by file, and require that every changed file passes.

Editors and CI

Most editors can run PHPCS on save using the project's ruleset, which catches issues while you type. Then add a CI step: run composer install and composer lint on every pull request and fail the build on errors. Teams often add phpcompatibility/phpcompatibility-wp too, which checks that your code runs on every PHP version you declare in Requires PHP.

Summary

  • WPCS adds WordPress rules to PHP_CodeSniffer; install both per project with Composer and allow the installer plugin.
  • Use the WordPress or WordPress-Extra standard for plugins: the security sniffs live in Extra.
  • Commit a phpcs.xml.dist that excludes vendor/ and build output, sets minimum_wp_version, your prefixes and your text_domain, and relaxes the file name rule for PSR-4 folders.
  • Run phpcs to check and phpcbf to fix formatting automatically; fix security findings by hand.
  • Ignore a rule only on one line, by sniff code, with a reason, and run the checks in CI on every pull request.

Further reading