Using PHPCS in WordPress with the WPCS standard

Using PHPCS in WordPress with WPCS standard

In this article we’ll talk about what a PHPCS linter is, as well as look at the importance of using it on a team project, customizing it, integrating PHPCS with the WPCS coding standard, and using it in GitHub Actions.

What is a linter (Lint)

Linter – is a static code analysis tool that checks for errors, bugs, and vulnerabilities.
How and what to check the static analyzer is set in the configuration file.
Read more about Linter by the link Wiki.

There are different kinds of static analyzers: Phan, PHPStan, PSalm, PHPCS, etc.
You have to keep in mind that not all of them are completely static.
For example, the PHPStan analyzer uses under-the-hood hybrid reflection (Runtime Reflection and Static Reflection) for optimizing CPU time and RAM allocation.


Using Runtime Reflection can lead us to unpleasant side effects like: “Executing code of a class or function that is called in the analyzed file”. Read more by the link.

Although PHPCS, PHPStan, Phan, and PSalm are static analyzers, using they are different.
The last three are used for complex syntactic code analyses with building deeper AST (abstract syntax tree). They show us unused code, define type mismatch, and prevent runtime errors in turn allow us to use them while refactoring or supporting legacy code.

Further in the article, I would like to speak in more detail about PHPCS.

PHPCS (PHP_CodeSniffer)

PHPCS – is a tool (composer package) for simplified static code analysis based on AST construction, checking the code for compliance with coding standards such as PSR2, PSR12, or WPCS (WordPress Coding Standards).

PHP_CodeSniffer helps us:

  • Monitor compliance with uniform code standards (this is especially important when different people are working on the code).
  • Write sequential, easy-to-read, supported code.
  • Reduce the count of bugs and vulnerabilities.

Out-of-the-box Liner supports the following coding standards: MySource, PEAR, PSR 1, PSR 2, PSR 12, Squiz, and Zend.
If necessary, you can additionally set the standard you require, such as WPCS or even create your own.
It also supports the customization of coding standards for the specific needs of the project, which we will consider in this article.

PHPCS can be used from the command line or integrated into an IDE (e.g. PhpStrom), or it can be built into CI/CD processes.

The Composer package PHP_CodeSniffer has two binary files:

  • phpcs – code analysis script for coding standards violations.
  • phpcbf – a script to automatically correct these violations.

How to install

Let’s add PHPCS as a composer dependency:

composer require "squizlabs/php_codesniffer" --dev

The flag --dev is required, because PHPCS will be installed into dev dependencies of composer.json, which in turn will not be installed for the production environment, in case you use the right installing composer packages.

for production using only composer install --no-dev command

Add the directive scripts with commands for composer.json, so that you will be able to run phpcs and phpcbf by using composer.
This trick can help you avoid errors related to running the wrong version of the binary file because they are run by the Composer itself.

//..... 
  "require-dev": {
    "squizlabs/php_codesniffer": "^3.7"
  },
  "scripts": {
    "phpcs": "phpcs",
    "phpcbf": "phpcbf"
  }
}

Further, create a file PHPCSWorkExample.php in the project with intentionally broken code styles.

I would recommend copying the entire file from the repository (see commit). Because your IDE may change the formatting when you insert it.
<?php
 
class PHPCSWorkExample {
    private $name = 'test';
	function get_test_name() {
      return $this->name;
	}
}

The next step is to try to start the linter.
The flag --standard=PSR2 means that we will check the file index.php for compliance with PSR-2: Coding Style Standard.

composer run phpcs -- --standard=PSR2 ./PHPCSWorkExample.php

And what do we see?
– A list of errors that do not match the PSR2 standard of the PHPCSWorkExample.php file and at the end of our command ends with an error code.

> phpcs '--standard=PSR2' './PHPCSWorkExample.php'
FILE: /usr/src/myapp/PHPCSWorkExample.php
---------------------------------------------------------------------------------------------------
FOUND 8 ERRORS AFFECTING 4 LINES
---------------------------------------------------------------------------------------------------
 3 | ERROR | [ ] Each class must be in a namespace of at least one level (a top-level vendor name)
 3 | ERROR | [x] Opening brace of a class must be on the line after the definition
 6 | ERROR | [x] Spaces must be used to indent lines; tabs are not allowed
 6 | ERROR | [ ] Method name "PHPCSWorkExample::get_test_name" is not in camel caps format
 6 | ERROR | [ ] Visibility must be declared on method "get_test_name"
 6 | ERROR | [x] Opening brace should be on a new line
 7 | ERROR | [x] Line indented incorrectly; expected at least 8 spaces, found 6
 8 | ERROR | [x] Spaces must be used to indent lines; tabs are not allowed
---------------------------------------------------------------------------------------------------
PHPCBF CAN FIX THE 5 MARKED SNIFF VIOLATIONS AUTOMATICALLY
---------------------------------------------------------------------------------------------------
Time: 61ms; Memory: 6MB
Script phpcs handling the phpcs event returned with error code 2

Note the string in the output of the console:

//..
PHPCBF CAN FIX THE 5 MARKED SNIFF VIOLATIONS AUTOMATICALLY
//...

PHPCS prompts us to automatically fix the irregularities found by using the PHPCBF command.

How to use PHPCBF (PHP Code Beautifier and Fixer)

Let’s follow PHPCS‘ advice and try to fix errors by using the PHPCBF command:

composer run phpcbf -- --standard=PSR2 ./PHPCSWorkExample.php

We see that PHPCBF fixed 5 errors in the file, but there are 3 not fixed.

/usr/src/myapp # composer run phpcbf -- --standard=PSR2 ./PHPCSWorkExample.php
> phpcbf --standard=./phpcs.xml '--standard=PSR2' './PHPCSWorkExample.php'
PHPCBF RESULT SUMMARY
----------------------------------------------------------------------
FILE                                                  FIXED  REMAINING
----------------------------------------------------------------------
/usr/src/myapp/PHPCSWorkExample.php                   5      3
----------------------------------------------------------------------
A TOTAL OF 5 ERRORS WERE FIXED IN 1 FILE
----------------------------------------------------------------------
Time: 50ms; Memory: 6MB
Script phpcbf --standard=./phpcs.xml handling the phpcbf event returned with error code 1

Let’s check this and run the check codestyle command again

composer run phpcbf -- --standard=PSR2 ./PHPCSWorkExample.php

And indeed, we see that now we have only 3 errors, which automatically could not be fixed by phpcbf:

-----------------------------------------------------------------------------------------------
FOUND 3 ERRORS AFFECTING 2 LINES
-----------------------------------------------------------------------------------------------
 3 | ERROR | Each class must be in a namespace of at least one level (a top-level vendor name)
 7 | ERROR | Method name "PHPCSWorkExample::get_test_name" is not in camel caps format
 7 | ERROR | Visibility must be declared on method "get_test_name"
-----------------------------------------------------------------------------------------------

This happened because PHPCBF is not always able to fix all errors in the code.

This can happen for several reasons:

  • PHPCBF cannot fix errors if the code is syntactically incorrect, contains logical errors, or requires additional programming. In such cases, it is necessary to edit the code manually.
  • PHPCBF may not be configured to use all coding rules that are used on the project. If you use non-standard rules that are not supported by PHPCBF, it will not be able to fix the corresponding errors.
  • Some PHPCS rules need to be ignored manually when the situation demands it (About this below).

You can only fix such errors manually, relying on the using style guide and the messages from PHPCS in the console.

After all the manual corrections, our file will look like this:

<?php
namespace APP;
 
class PHPCSWorkExample
{
    private $name = 'test';
    public function getTestName()
    {
        return $this->name;
    }
}

Remember that automatic code correction tools can lead to unwanted changes in your code, so you should always be careful and check these changes manually.

If you find a lot of errors, first I recommend to commit the code and then run the PHPCBF command and compare the files difference. Then you will be sure that autofix was successful.

WPCS (WordPress Coding Standards)

WPCS (WordPress Coding Standards) – is a set of rules for writing code for WordPress.

WordPress Coding Standards for PHPCS – is a collection of PHP_CodeSniffer rules (sniffs) for checking code written for WordPress.

How to install WPCS standard

You can install the WPCS set of rules by using Composer:

composer require wp-coding-standards/wpcs --dev

As of this writing 13.04.2023, the WPCS sniffer package does not work with PHP above 7.4

To install the package with the fixes, use the dev-develop branch:
composer require wp-coding-standards/wpcs:"dev-develop" --dev .
Read more at the link issues/2087

After installing the WPCS standard, run the command to see which standards are currently connected to PHPCS:

composer run phpcs -- -i
The installed coding standards are MySource, PEAR, PSR1, PSR2, PSR12, Squiz and Zend

But where are our WordPress Coding Standards?

In order to pull them up, we need to add a package that makes it easy to use the new PHP_CodeSniffer standards, without having to manually configure them:

composer require --dev dealerdirect/phpcodesniffer-composer-installer

Now if we run the command composer run phpcs -- -i , we see that all WPCS standards are available to us:

The installed coding standards are MySource, PEAR, PSR1, PSR2, PSR12, Squiz, Zend, WordPress, WordPress-Core, WordPress-Docs and WordPress-Extra

I suggest that you check whether our PHPCSWorkExample.php complies with the WPCS standard:

composer run phpcs -- --standard=WordPress ./PHPCSWorkExample.php

And we get the result

> phpcs '--standard=WordPress' './PHPCSWorkExample.php'
FILE: /usr/src/myapp/PHPCSWorkExample.php
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
FOUND 16 ERRORS AND 1 WARNING AFFECTING 9 LINES
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
  1 | ERROR   | [ ] Filenames should be all lowercase with hyphens as word separators. Expected phpcsworkexample.php, but found PHPCSWorkExample.php.
  1 | ERROR   | [ ] Class file names should be based on the class name with "class-" prepended. Expected class-phpcsworkexample.php, but found PHPCSWorkExample.php.
  1 | ERROR   | [ ] Missing file doc comment
  4 | WARNING | [ ] Found precision alignment of 1 spaces.
  4 | ERROR   | [x] Whitespace found at end of line
  5 | ERROR   | [ ] Missing doc comment for class PHPCSWorkExample
  6 | ERROR   | [x] Opening brace should be on the same line as the declaration for class PHPCSWorkExample
  7 | ERROR   | [x] Tabs must be used to indent lines; spaces are not allowed
  7 | ERROR   | [ ] Missing member variable doc comment
  9 | ERROR   | [x] Tabs must be used to indent lines; spaces are not allowed
  9 | ERROR   | [x] Expected exactly one space between closing parenthesis and opening control structure; "
    |         |     " found.
  9 | ERROR   | [ ] Method name "getTestName" in class PHPCSWorkExample is not in snake case format, try "get_test_name"
  9 | ERROR   | [ ] Missing doc comment for function getTestName()
 10 | ERROR   | [x] Tabs must be used to indent lines; spaces are not allowed
 10 | ERROR   | [x] Opening brace should be on the same line as the declaration
 11 | ERROR   | [x] Tabs must be used to indent lines; spaces are not allowed
 12 | ERROR   | [x] Tabs must be used to indent lines; spaces are not allowed
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
PHPCBF CAN FIX THE 9 MARKED SNIFF VIOLATIONS AUTOMATICALLY
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
Time: 750ms; Memory: 6MB
Script phpcs handling the phpcs event returned with error code 2

Run the PHPCBF auto fixer again, and we are left with 7 errors in 4 lines.

--------------------------------------------------------------------------------------------------------------------------------------------------------------
 1 | ERROR | Filenames should be all lowercase with hyphens as word separators. Expected phpcsworkexample.php, but found PHPCSWorkExample.php.
 1 | ERROR | Class file names should be based on the class name with "class-" prepended. Expected class-phpcsworkexample.php, but found PHPCSWorkExample.php.
 1 | ERROR | Missing file doc comment
 5 | ERROR | Missing doc comment for class PHPCSWorkExample
 7 | ERROR | Missing member variable doc comment
 9 | ERROR | Method name "getTestName" in class PHPCSWorkExample is not in snake case format, try "get_test_name"
 9 | ERROR | Missing doc comment for function getTestName()
--------------------------------------------------------------------------------------------------------------------------------------------------------------

When you have fulfilled all the PHPCS code style requirements, your file will be renamed class-phpcsworkexample.php and look as follows:

<?php
/**
 * PHPCSWorkExample
 *
 * @package APP
 */
namespace APP;
/**
 * PHPCSWorkExample
 */
class PHPCSWorkExample {
	/**
	 * Test name.
	 *
	 * @var string $name
	 */
	private $name = 'test';
	/**
	 * Get test name.
	 *
	 * @return string
	 */
	public function get_test_name() {
		return $this->name;
	}
}

Some bugs that PHPCS has thrown out may seem redundant, and may only make User Expirience worse when dealing with code such as “strict naming of class files”.

– And yes, you’re right, in the next chapter we will learn how to configure custom WPCS rules for our project.

Configuring WPCS rules (Best Practices)

The PHP_CodeSniffer allows developers to configure their own coding standards by creating a phpcs.xml file with their own set of rules. Within the file, we can use existing coding standards, and modify them to make them simpler or stricter for the needs of our project.

Basic settings

First, you need to create a file phpcs.xml in the root of project with the next content:

<?xml version="1.0"?>
<ruleset name="WordPress Coding Standards">
    <description>A custom set of code standard rules to check for WordPress code.</description>
    <!-- How to scan -->
    <arg value="sp"/><!-- Show sniff and progress -->
    <arg name="basepath" value="./"/><!-- Strip the file paths down to the relevant bit -->
    <arg name="extensions" value="php"/>
    <arg name="parallel" value="10"/><!-- Enables parallel processing when available for faster results. -->
    <arg name="cache" value=".phpcs.cache"/>
    <!-- What to scan -->
    <file>./</file>
    <!-- Exclude basic project directories -->
    <exclude-pattern>*/.idea/*</exclude-pattern>
    <exclude-pattern>*/.github/*</exclude-pattern>
    <exclude-pattern>*/vendor/*</exclude-pattern>
    <exclude-pattern>*/node_modules/*</exclude-pattern>
    <exclude-pattern>*/assets/*</exclude-pattern>
    <exclude-pattern>wp-content/uploads/</exclude-pattern>
    <rule ref="WordPress"/>
</ruleset>

In the config, we specified that we use a WordPress ruleset, scan only php files, display the scanning process, run in 10 threads to speed up analysis and cache the result in a .phpcs.cache file.

Now that our config is ready, all we have to do is to specify where PHPCS will pick it up from.
Go to the composer.json file and fix the scripts block:

  "scripts": {
    "phpcs": "phpcs --standard=./phpcs.xml",
    "phpcbf": "phpcbf --standard=./phpcs.xml"
  },

Now you can execute commands with this config in mind

composer run phpcs
composer run phpcbf

Add Cache

Be sure to add a directive that caches PHPCS results, as this will save a huge amount of time when the linter is run again:

<arg name="cache" value=".phpcs.cache"/>

What to scan

I recommend using the “Scan everything but what is forbidden” approach.
I.e. we need to allow scanning everything in directories /mu-plugins/, /plugins, /themes except what is forbidden. And in the forbidden be third-party plugins and themes, which you can prescribe in the PHPCS config as exceptions manually.

<!-- .... --> 
   <!-- What to scan -->
    <file>wp-content/mu-plugins/</file>
    <file>wp-content/plugins/</file>
    <file>wp-content/themes/</file>
    <!-- Exclude "Third-party Themes" -->
    <exclude-pattern>wp-content/themes/twentytwenty/</exclude-pattern>
    <!-- Exclude "Third-party Plugins" -->
    <exclude-pattern>wp-content/plugins/advanced-custom-fields-pro/</exclude-pattern>
<!-- .... -->

Why is it necessary to do this?
The answer is simple: If the developer creates a new theme or plugin, your code will default to the PHPCS scanning area and your CI will never miss a standards violation, which is not the case with the “Scan only what is allowed” approach. If you use the second one, you will regularly see code with violates the standards.

Don’t use YodaConditions

Yoda Conditional is one of the most annoying rules of the WPCS standard, which brings more problems than benefits.
Here is a quote from the HumanMade CodeStyle Convention that details the pain:

<!–
OK, real talk right now. Yoda conditions are ridiculous.
The fundamental problem that Yoda conditions attempts to solve is:
the equality operator is very close to the assignment operator.
It’s easy to mess up and accidentally assign to the variable you’re
trying to check.
Here’s the thing though. Yoda conditions just don’t read correctly
in the code flow. They require you to change the way you think
about the control flow.
Rather than forcing every conditional to be backwards, why not ban
assignment from conditionals? You never really *need* to assign in
a conditional.
So, here’s where I stand: no to Yoda conditions. Yes to banning
assignment in conditionals.
–>

Source: https://github.com/humanmade/coding-standards/blob/master/HM/ruleset.xml

Prohibit the use of YodaConditional in phpcs.xml

<!-- .... -->
<rule ref="WordPress">
    <!-- .... -->
    <exclude name="WordPress.PHP.YodaConditions" />
    <!-- .... -->
</rule>
<!-- .... -->
<!-- Prohibit Yoda Conditions expressions -->
<rule ref="Generic.ControlStructures.DisallowYodaConditions"/>
<!-- .... -->

Disabling PHPDocBlock

The WPCS rules for comments are quite strict and redundant.
If you’re writing smart code, you shouldn’t have to write a comment on every property/method/class/file
The names of your functions, classes, files, and properties should describe what is happening inside or what entity they belong to.

Write comments where it is only necessary.

We’ll loosen the commenting rules with the following settings:

<!-- .... -->
<rule ref="WordPress">
        <!-- .... -->   
        <exclude name="Generic.Commenting.DocComment.MissingShort" />
        <!-- Disable dot of end string docblock -->
        <exclude name="Squiz.Commenting.InlineComment.InvalidEndChar" />
        <!-- .... -->
</rule>
<!-- .... -->
 <!-- Disable comments blocks -->
    <rule ref="WordPress-Docs">
        <exclude name="Squiz.Commenting.FileComment.Missing" />
        <exclude name="Squiz.Commenting.FileComment.MissingPackageTag" />
        <exclude name="Squiz.Commenting.ClassComment.Missing" />
        <exclude name="Squiz.Commenting.FunctionComment.Missing" />
        <exclude name="Squiz.Commenting.FunctionComment.SpacingAfterParamType" />
        <exclude name="Squiz.Commenting.VariableComment.Missing" />
        <exclude name="Squiz.Commenting.FunctionComment.MissingParamComment" />
        <exclude name="Squiz.Commenting.FunctionComment.MissingParamTag" />
    </rule>
<!-- .... -->

Class files in CamelCase or Snake_Case

In WPCS rules, there are strict rules for class file names, and we had to rename our file into class-phpcsworkexample.php to comply with them, which does not make the file names readable – you need a knack for extracting entity names from them.
These class names are not at all consistent with the accepted standards in PHP, and moreover, do not lend themselves to sound logic.
I suggest disabling these rules, which will allow us to name class files in CamelCase (PSR) or Snake_Case format (I suggest following the CamelCase, because it is a PSR format).

<!-- .... --> 
   <rule ref="WordPress">
        <!-- .... -->
        <exclude name="WordPress.Files.FileName.InvalidClassFileName" />
        <exclude name="WordPress.Files.FileName.NotHyphenatedLowercase" />
        <!-- .... -->
    </rule>
<!-- .... -->

A ban on the use of functions

Some of the functions should be banned for security reasons. Others are debug functions and should not be in production. Others can cause potential bugs in refactoring. For these purposes, in PHPCS you can add the block of rules below.
You can also expand it with your own functions.
For example: “deprecated” by functions in your project that need to be refactored or replaced.

<!-- .... -->
    <!-- Forbidden functions -->
    <rule ref="Generic.PHP.ForbiddenFunctions">
        <properties>
            <property name="forbiddenFunctions" type="array">
                <element key="delete" value="unset"/>
                <element key="print" value="echo"/>
                <element key="create_function" value="null"/>
                <element key="sizeof" value="count"/>
                <!-- <element key="var_dump" value="null"/> duplicate of WP rule-->
                <element key="print_r" value="null"/>
                <element key="eval" value="null"/>
                <element key="compact" value="null"/>
            </property>
        </properties>
    </rule>
<!-- .... -->

AutoEscaped functions

Some of your functions may already contain escaping inside and unfortunately, WPCS will not understand it.
In this case, you have two ways around it:

  • Use phpcs:ignore in the code where you use this function
    //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
  • Add to the auto-escaped functions block in the phpcs.xml file
<!-- .... -->
  
  <!-- AutoEscaped functions -->
    <rule ref="WordPress.Security.EscapeOutput">
        <properties>
            <property name="customAutoEscapedFunctions" type="array">
                <element value="rd_render_attributes" />
                <element value="rd_get_picture_html" />
            </property>
        </properties>
    </rule>
<!-- .... -->

I recommend the 2nd option because your code will be cleaner.

Allow PHP short syntax

<!-- .... -->
    <rule ref="WordPress">
        <!-- Allow short ternary syntax and short arrays and short open tag -->
        <exclude name="Generic.Arrays.DisallowShortArraySyntax" />
        <exclude name="WordPress.PHP.DisallowShortTernary.Found" />
        <exclude name="Generic.PHP.DisallowShortOpenTag.EchoFound" />
    </rule>
<!-- .... -->
    <!-- Disallow long array syntax. -->
    <rule ref="Generic.Arrays.DisallowLongArraySyntax.Found"/>
<!-- .... -->

In the above rules, we allow Short Array Syntax and disallow Long Array Syntax in order to maintain a single-style convention

<?php
//available
$data = [
	'one' => 'one-data',
	'two' => 'two-data',
	'three' => 'three-data',
];
//unvailable
$data = array(
	'one' => 'one-data',
	'two' => 'two-data',
	'three' => 'three-data',
);

Also, allow Ternary Syntax

<?php
//available
$data = $request ? $request->data : null;

And allow Short Open Tag with Echo <?= :

<!--available -->
<p><?= $a ?><p>

It should not be confused with the ASP style tag <? , because the second one was deprecated in PHP and is no longer supported, while <?= allows us to write more concise code.

Using “/” in hook names

Allowing / in hook names allow us to write expressive hooks reminiscent of the concept of namespaces:

<?php 
//available
$path_to_font = apply_filters( 'rd/plugin-name/class-name/component-name', $data );
<!-- .... -->   
    <!-- Allow symbol `/` in hook names. -->
    <rule ref="WordPress.NamingConventions.ValidHookName">
        <properties>
            <property name="additionalWordDelimiters" value="/" />
        </properties>
    </rule>
<!-- .... -->

In our project, the development team fell in love with this code style quite quickly, because everyone had a direct analogy with the namespaces concept in PHP.

Instruction nesting level

This rule defines the nesting level of instructions if else, while, forach, for etc, and outputs an error if it is exceeded:

<!-- .... -->    
   <rule ref="Generic.Metrics.NestingLevel">
        <properties>
            <property name="absoluteNestingLevel" value="3"/>
        </properties>
    </rule>
<!-- .... -->   

This rule inevitably affects the quality of the code and its support, with a high level of nesting the code becomes difficult to read.
Here is an example, at nesting level 2 we can write the following code:

<?php
if ( $data['request'] ) {
	if ($data['request']['method'] === 'GET' ) {
		echo "Hello World!";
	}
}

But if we set the value absoluteNestingLevel by 1, then we get an error.

I recommend you set it to 3, but sometimes there are complex legacy projects where you have to set it to 4. In general, no one prevents you from adding 2 rules, for legacy code with absoluteNestingLevel=4 and for the new code with absoluteNestingLevel=3. You only need to specify the directories for each rule block.

Checking the code for compatibility with WordPress versions

WPCS has a sniffer to check the code for the minimum compatible version of WordPress. This sniffer checks for deprecated functions, classes, and function parameters.

For deleted features before minimum_wp_version, you get an error. When determining the deprecated features between minimum_wp_version and the current release, you get a warning.

For example: the function add_object_page() was deprecated in WP 4.5. If usage of this function is detected, the sniff will throw an error if the minimum_wp_version is 4.6 or higher. It will throw a warning if the minimum_wp_version is 4.5 or lower.

Add this rule to our phpcs.xml:

    <!-- Minimum WP version to check for usage of deprecated functions, classes and function parameters -->
    <config name="minimum_wp_version" value="5.4.1"/>

Prior to WordPressCS 3.0.0, the setting name for CLI was called minimum_supported_wp_version.

Specify the version of WordPress you’re currently using, or the one you want to upgrade to, to check your code for compatibility.

You can read more about minimum_wp_version at this link.

Checking code for compatibility with PHP versions

Using PHPCompatibilityWP, you can analyze the code base of your WordPress project for compatibility with different versions of PHP.

composer require --dev phpcompatibility/phpcompatibility-wp:"*"

If all went well by running the command below, you will see PHPCompatibility and PHPCompatibilityWP in available code styles

vendor/bin/phpcs -i

To make our parser check for compatibility with PHP versions, we add the following rules to phpcs.xml :

<!-- Check PHP version compatibility -->
<rule ref="PHPCompatibilityWP"/>
<!-- Check for cross-version support for PHP 7.2 and higher. -->
<config name="testVersion" value="7.2-"/>

Our analyzed code will now be checked for compatibility with the minimum version of PHP 7.2. And this will allow us to check the code for deprecated or removed PHP functions as well as detect the use of new unsupported PHP features.

Other custom rules

For other custom rules, you can find information by the link.

Ignoring rules in code

In addition to configuring the config, PHPCS has the ability to ignore the rules point-by-point in the code.

You just need to add to the ignored line // phpcs:ignore .

<?php
$test = "Hello World";
echo $test; //phpcs:ignore

I recommend specifying the rule you ignore because ignoring all the rules // phpcs:ignore you can miss a vulnerability in your code, which will be added afterward.

The following example demonstrates ignoring only concrete rule WordPress.DB.SlowDBQuery.slow_db_query_meta_query

<?php
//......
$duplicated_posts = new WP_Query(
	[
		'post_type'  => 'any',
		'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
			[
				'key'   => META_DP_ORIGINAL,
				'value' => $post_id,
			],
		],
	]
);

You can get the names of the rules that PHPCS complains about from the linter error log in a console.

How to use PHPCS with GitHub Actions

Using PHPCS together in the GitHub Actions CI allows an automated code review before merging MR/PR into the main branch. If an error is found, the GitHub Action Job terminates with a failure and reports it in the code review. Inside the Job itself, you are able to read the error logs.

Here is the code of the working action:

name: Workflow with PHPCS linter
env:
  PHP_VER: "8.2"
  COMPOSER_VER: "2.5.1"
on:
  push:
    branches: [ "main", "dev" ]
  pull_request:
    types: [synchronize, opened, reopened]
jobs:
  php-ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Cache Composer dependencies
        uses: actions/cache@v3
        with:
          path: /tmp/composer-cache
          key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
      - uses: php-actions/composer@v6
        with:
          php_version: ${{ env.PHP_VER }}
          version: ${{ env.COMPOSER_VER }}
      - name: Get PHPCS Cache
        id: phpcs-cache
        run: |
          echo "file=.phpcs.cache" >> $GITHUB_OUTPUT
      - uses: actions/cache@v3
        with:
          path: ${{ steps.phpcs-cache.outputs.file }}
          key: ${{ runner.os }}-phpcs-${{ hashFiles('**/.phpcs.cache') }}
          restore-keys: |
            ${{ runner.os }}-phpcs-
      - name: Run PHPCS
        run: composer run phpcs

What’s going on here?

  • As with local env, we first need to get the code from git, for which GitHub Action uses the ready-made actions/checkout@v3 action.
  • Then we need to install the composer dependencies, to do this in GitHub Action we use php-actions/composer@v6 action.
  • When packages are installed, we move on to Get PHPCS Cache job in such we get PHPCS cache, to speed up our job.
  • After the cache file is received, we can execute the Run PHPCS .
WPCS in WordPress with Github Actions

In the picture, you can see the result of GitHub Action jobs. In the expanded item you can see the output of PHPCS.
You can see a real example of PHPCS with configured WPCS rules and Github Action by the link.

Full source code

The full code of this article you can be found in the GitHub repository.

The result

In this article we covered what PHPCS is and the differences between PHPCS and other static analyzers. We took a look at the WPCS standard and how you can set up your own code style. We wrote a simple GitHub Action CI to run PHPCS.

This not-so-basic information should be enough for you to use PHPCS with WPCS on a WordPress project and improve code quality while team development.
Don’t be shy to experiment and offer something new on your project.

2 responses to “Using PHPCS in WordPress with WPCS standard”

  1. Many thanks for the detailed and very helpful article. It’s very relieving to hear that others are also sceptical about the Yoda conditions and don’t see the point of using the unnecessarily long array syntax and that there are ways to get rid of these questionable rules.

    If I understand you correctly, are you importing the entire WordPress directory into your IDE?

    So far I’ve only imported the plugin directory into VS Code when I’ve developed a plugin and the themes directory when I’ve developed a theme. This makes the directory tree clearer, but leads to WordPress’ own functions being labelled with a “Call to unknown function” warning. With PHP Intelephense there is the possibility to add WordPress stubs, which means that the WP functions are no longer marked as unknown. Do you know a trick for phpcs or would you generally recommend always importing the entire WordPress directory?

    • Hi, erdmann. I’m glad you liked the article.
      I’m developing platform for 100+ sites, that includes plugins, themes, mu-plugins, configs files, wp core as well.
      Answering on you questions:

      If I understand you correctly, are you importing the entire WordPress directory into your IDE?

      – Yes I have WordPress directory into my IDE. It’s ok for my platform and it’s ok for site developing.

      This makes the directory tree clearer, but leads to WordPress’ own functions being labelled with a “Call to unknown function” warning.

      I’m working with PHPStorm and into the setting I can configure custom WordPress path if I needed to develop a plugin or a theme:
      Custom WordPress path in PHPStorm
      In the PHPStorm everything is out-of-the-box, I recommend you use it.
      You can try to find something in VSCode or add wordpress core inside a theme or plugin and add this directory to the .gitignore.

      Do you know a trick for phpcs or would you generally recommend always importing the entire WordPress directory?

      I’m not sure that I completely understand you in this question, but. PHPCS is a simple static analyzer that could not build AST tree for analyzing whole project as it do PHPStan or Phan. It just checks code styles or some security issues, performance issues (some wpcs rules), but just simple rules.
      In that fact you don’t need to include WordPress code into phpcs config.

Leave a Reply

Your email address will not be published. Required fields are marked *