Drupal 9: Using PHP_CodeSniffer To Inspect Custom Code

Drupal 9: Using PHP_CodeSniffer To Inspect Custom Code

13th November 2022 - 24 minutes read time

Drupal has a number of coding standards and best practices that govern the way code should be written. This has many benefits but can allow for a consistent and maintainable code to be created.

All Drupal modules and themes are written with coding standards in mind and that allows Drupal developers to look at any project and see a similar style of code. If you ever submit code to Drupal or a contributed project you will be required to adhere to these coding standards, so it makes sense to get used to knowing about them.

The main tool that is used to test coding standards in Drupal is PHP_CodeSniffer, which is a widely used static analysis tool for testing PHP coding standards. It comes with a set of coding standards built in, but it is possible to add the Drupal coding standards for the tool to use.

In this article we will look at installing PHP_CodeSniffer and adding the Drupal coding standards to it. We will also looking at some of the more common coding standards violations that might be encountered and how to solve them.

Installing PHP_CodeSniffer

There are two ways in which to install PHP_CodeSniffer, globally and as a dependency on the project you are working on. Each of these approaches has different pros and cons, so we will address those whilst looking at how to install each.

Installing PHP_CodeSniffer In A Project

The simplest way to get up and running with PHP_CodeSniffer and Drupal is by installing the package within a project. When using PHP_CodeSniffer in Drupal we install the Drupal Coder project, which will install the package and all of the needed standards that are required to inspect the code. The Drupal Coder project is installed as a dev dependency in order to prevent it from reaching our production environments.

To install the Dupal Coder project as a dev dependency run this command.

composer require --dev drupal/coder

See the section on installing the package globally for more information on what packages are installed in this step.

That's pretty much it, everything you need to run PHP_CodeSniffer is now installed, along with the coding standards needed to inspect the code.

In order to run the tool you can run the binary directly like this.

$ ./vendor/bin/phpcs --version
PHP_CodeSniffer version 3.7.1 (stable) by Squiz (http://www.squiz.net)

Or, depending on the system you are using, you might be able to run the tool without referencing the binary directly.

$ phpcs --version
PHP_CodeSniffer version 3.7.1 (stable) by Squiz (http://www.squiz.net)

Whilst installing PHP_CodeSniffer this way is simple, it does have a couple of drawbacks. The main problem here is that you now have a local dependency with PHP_CodeSniffer and the Coder project, which means you might be faced with composer dependency issues in the future. Secondly, if you want to run the tool on other Drupal projects you need to install it on those projects as well.

It is, however, simple to install like this and can be run on any continuous integration service easily since it's packaged along with the source code.

Installing PHP_CodeSniffer Globally

Another way to install PHP_CodeSniffer is as a global package, which allows you to run PHP_CodeSniffer on any project on your computer. This might actually be your preferred mechanism of doing this as it allows you to prevent dependencies of PHP_CodeSniffer interfering with your project dependencies since it is installed outside of your project.

In addition to installing the tool, there is a significant amount of configuration needed to get this running correctly. First though, let's look at installingt he rool.

Run the following command to install the Drupal Coder package globally using composer. This is just like installing a package in a project, but the use of the global keyword adds the package to your global composer install, which will be in your user directory.

composer global require drupal/coder

This installs several packages, which you can view using the following command (assuming you've not installed anything else). This will show you the following output, with references to all of the packages that we just installed.

$ composer global show -P
Changed current directory to /Users/username/.composer
dealerdirect/phpcodesniffer-composer-installer /Users/username/.composer/vendor/dealerdirect/phpcodesniffer-composer-installer
drupal/coder                                   /Users/username/.composer/vendor/drupal/coder
phpstan/phpdoc-parser                          /Users/username/.composer/vendor/phpstan/phpdoc-parser
sirbrillig/phpcs-variable-analysis             /Users/username/.composer/vendor/sirbrillig/phpcs-variable-analysis
slevomat/coding-standard                       /Users/username/.composer/vendor/slevomat/coding-standard
squizlabs/php_codesniffer                      /Users/username/.composer/vendor/squizlabs/php_codesniffer
symfony/polyfill-ctype                         /Users/username/.composer/vendor/symfony/polyfill-ctype
symfony/yaml                                   /Users/username/.composer/vendor/symfony/yaml

The output of this command is important. Pay close attention to the first line here; the "Changed current directory to" part. This tells you where your global composer configuration is held and is key to the next steps in this process. In this example I am running this on OSX and so the path is /Users/username/.composer, but on other linux systems this is /home/username/.config/composer. On Windows this might be C:/Users/username/AppData/Roaming/Composer. Make a note of this path at it will be used later in the configuration of the tool.

Once you have that information in hand you can then proceed onto the next step and configure PHP_CodeSniffer.

Configure Global PHP_CodeSniffer

As PHP_CodeSniffer is installed globally through composer we need it to be available to to run as a normal program. Currently, however, your system will not know about the program as it is contained in a directory that isn't usually looked at by the operating system. This means that you will see an error when you try to run the PHP_CodeSniffer command itself "phpcs".

$ phpcs --version
bash: phpcs: command not found

To solve this we need to add the composer bin directory to your $PATH variable. On Mac/Linux this is a case of editing your ~/.profile, ~/.bash_profile, ~/.bashrc or ~/.zshrc file and adding the following line.

export PATH="/Users/username/.composer/vendor/bin:$PATH"

Some examples will tell you to add this as a relative link using the "~" character to point to your user directory. This can lead to the path not being translated correctly; so instead add the full path to the composer "vendor/bin" directory. Note that on some systems this is stored at the location /home/username/.config/composer.

Once you have updated the PATH variable you should be able to run PHP_CodeSniffer like this.

$ phpcs --version
PHP_CodeSniffer version 3.7.1 (stable) by Squiz (http://www.squiz.net)

The next step is to inform PHP_CodeSniffer of the coding standards that we want to use. This is done by using the --config-set flag and setting the installed_paths configuration setting. We actually need to add two standards here, the Slevomat coding standard (which is what the Drupal coding standards are based on) and the two Drupal coding standards (Drupal and DrupalPractice). These need to be added to the PHP_CodeSniffer configuration in one pass, which is possible using the following command.

phpcs --config-set installed_paths ~/.composer/vendor/drupal/coder/coder_sniffer,~/.composer/vendor/drupal/slevomat/coding-standard

You can double check this worked by using the -i flag on the phpcs tool, which will print out the installed coding standards.

$ php -i
The installed coding standards are PEAR, Zend, PSR2, MySource, Squiz, PSR1, PSR12, Drupal, DrupalPractice, VariableAnalysis and SlevomatCodingStandard

You should see the Drupal, DrupalPractice and SlevomatCodingStandard standards. All three of these standards are required in order to run full coding standards checks on Drupal code.

If you find that those standards are not correctly installed then you can change the paths supplied to the -i flag to be absolute (rather than relative using the "~" character).

phpcs --config-set installed_paths /Users/username/.composer/vendor/drupal/coder/coder_sniffer,/Users/username/.composer/vendor/drupal/slevomat/coding-standard

The two Drupal coding standards we just installed are as follows:

  • Drupal - This looks at the code to ensure that it adheres to the available coding standards. This involves things like whitespace, naming conventions, array syntax, and other formatting problems.
  • DrupalPractice - This standard looks at the code to stop any common problems that might occur. Things like the use of global constants, lack of translations, dependency injection not being used, security issues being introduced or are all part of this standard.

The SlevomatCodingStandard provides a lot of the rules that the Drupal coding standards are built on.

With the tool installed and all of these standards in place you can now start using them to inspect your Drupal code.

Using PHP_CodeSniffer

What we are aiming for in our inspection is to check all of the custom code we have written inside a project. We do not need to inspect the Drupal core or contributed code (unless we are going to be submitting that code to those contributed projects). As a result, there are probably three directories that we want to test here.

Assuming your Drupal project lives in a directory called "web", these custom directories will be:

  • web/modules/custom - For custom Drupal modules.
  • web/themes/custom - For custom Drupal themes.
  • web/profiles/custom - For custom Drupal install profiles.

The PHP_CodeSniffer tool is simple to use, although we do have to pass quite a few parameters in order to get the effect we want. The following command will inspect all code within a directory using the Drupal and DrupalPractice coding standards.

phpcs --standard=Drupal,DrupalPractice --extensions=php,module,inc,install,test,profile,theme,css,info,txt,md,yml --ignore=node_modules,bower_components,vendor web/modules/custom

Let's break this command into the component parts.

  • phpcs - The PHP_CodeSniffer tool
  • --standard=Drupal,DrupalPractice - The standards that should be used to inspect the code.
  • --extensions=php,module,inc,install,test,profile,theme,css,info,txt,md,yml - The file extensions that we will include in the inspection.
  • --ignore=node_modules,bower_components,vendor - Ignore these directories from the coding standards checks.
  • web/modules/custom - The directory we want to start out code inspection from. By default, PHP_CodeSniffer will inspect directories recursively. Note that the directory needs to exist or PHP_CodeSniffer will throw an error.

This should either produce no output at all (if no issues were found), or a list of issues that are connected to the coding standards we stipulated here.

Note that if you see an error like the following.

ERROR: Referenced sniff "SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator" does not exist

It means that the SlevomatCodingStandard isn't installed correctly. Make sure that this standard is available to the PHP_CodeSniffer tool.

Configuring PHP_CodeSniffer With XML

You may have noticed that the number of arguments needed to run PHP_CodeSniffer is a little long. Thankfully, those arguments can be set in an XML that PHP_CodeSniffer will automatically pick up when run.

The XML file can be called one of .phpcs.xml, phpcs.xml, .phpcs.xml.dist, or phpcs.xml.dist.

Create a file called phpcs.xml in the root of your project (i.e. where you run the "phpcs" command) and add the following contents. This will mimic the settings added as command line parameters in the previous section.

<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="myproject_phpcs_configuration">

  <!-- Use Drupal and DrupalPractice standards. -->
  <rule ref="Drupal"/>
  <rule ref="DrupalPractice"/>

  <!-- Inluce the extensions of the files we want to test. -->
  <arg name="extensions" value="php,module,inc,install,test,profile,theme,css,info,txt,md,yml"/>

  <!--Exclude folders used by common frontend tools. These folders match the file_scan_ignore_directories setting in default.settings.php-->
  <exclude-pattern>*/bower_components/*</exclude-pattern>
  <exclude-pattern>*/node_modules/*</exclude-pattern>
  <!--Exclude third party code.-->
  <exclude-pattern>*/vendor/*</exclude-pattern>

  <!-- Inspect the following directories. -->
  <file>./web/modules/custom</file>
  <file>./web/profiles/custom</file>
  <file>./web/themes/custom</file>

</ruleset>

With that file in place we can now run the tool by just typing "phpcs". The configuration file will be picked up automatically and used in the inspection of the code. Note that all of the directories you want to inspect need to exist in order for the tool to run correctly. If you don't have any custom Drupal install profiles then remove that directory from the configuration.

Take a look at the Annotated Ruleset in the PHP_CodeSniffer documentation or more information on what options are available in the XML file syntax.

The good thing about using a phpcs.xml file is that the arguments in the file are then considered as defaults. If you supply any argument to the command line then it will override this default value.

For example, to run the phpcs analysis on a single PHP class we could do the following.

phpcs web/modules/custom/mymodule/src/Controller/TestController.php

The output from this command would contain only Drupal coding standards problems relating to that file. The supplied file arguments from the phpcs.xml file are ignored.

Some Common Problems

When running PHP_CodeSniffer, you might see a number of common problems being reported. This section looks at a few common problems that you might encounter and how to solve them.

Line Indented Incorrectly

xx | ERROR | [x] Line indented incorrectly; expected 2 spaces, found 4

This is a simple error that will likely appear a lot in your codebase. The number of spaces might be different here, but the message will be the same.

Take the following code that contains a function with a single line that is indented incorrectly.

function mymodule_preprocess_webform_confirmation(&$variables) {
    $variables['#attached']['library'][] = 'mymodule/a.library';
}

The number of spaces to be used for all indentation in Drupal is 2. This means that the above code just needs two spaces removing in order to make it compliant.

function mymodule_preprocess_webform_confirmation(&$variables) {
  $variables['#attached']['library'][] = 'mymodule/a.library';
}

This might seem trivial, but indentations are easy to get wrong when, for example, defining a large array or when separating out code onto separate lines. All of this is possible in PHP, but getting the indentation correct first time is sometimes a challenge.

Short Array Syntax

xx | ERROR   | [x] Short array syntax must be used to define arrays

This one is pretty straight forward. The following array definition syntax is not standards compliant.

$build = array();

You must instead use the short array syntax when defining arrays.

$build = [];

Missing Doc Comments

xx | ERROR   | [x] Missing class doc comment
xx | ERROR   | [x] Missing function doc comment

This is caused by a lack of doc comments on classes and functions. For example, take the following controller class, which produces both of these errors.

<?php

namespace Drupal\mymodule\Controller;

use Drupal\Core\Controller\ControllerBase;

class TestController extends ControllerBase {

  public function someAction() {
    $build = [];
    // Action contents here.
    return $build;
  }

}

This can be solved by adding comments to the class and function definitions within the class. The class comment should give an indication of what the class does. The function comments must also include any params and return arguments.

This is the fully compliant class with the doc comments added.

<?php

namespace Drupal\mymodule\Controller;

use Drupal\Core\Controller\ControllerBase;

/**
 * Test controller for testing purposes.
 */
class TestController extends ControllerBase {

  /**
   * Callback for route mymodule_display_page.
   *
   * @return array
   *   The page render array.
   */
  public function someAction() {
    $build = [];
    // Action contents here.
    return $build;
  }

}

Dependency Injection Not Used

xx | WARNING | [ ] \Drupal calls should be avoided in classes, use dependency injection instead

This issue can be easily done, especially when quickly prototyping out functionality. It will, however, make your code more difficult to maintain and test, which is why it is flagged as an issue.

Take the following two lines of code, which both produce the same result of loading in the current user object. 

$user = \Drupal::currentUser();
$user = \Drupal::service('current_user');

Using this code in module files is ok (although not completely ideal), but using them in your classes will cause PHP_CodeSniffer to flag this issue. What you need to do here is instead inject the "current_user" service into the class, rather than hard code it in this way.

The solution to this issue has a number of different outcomes, and depends on what sort of object or service you are injecting. For more information on how to use services and dependency injection in Drupal 8+ can be found in our article on the subject.

Please let us know if you would like to see this done in more detail. Converting classes from using static calls to dependency injected calls can be a complex process so this can be the basis of another article if needed.

Open Routes

xx | WARNING | Open page callback found, please add a comment before the line why there is no
   |         | access restriction

When setting up public facing routes in a modules *.routing.yml file it is sometimes easier to set the "_access" requirement to be "true", which allows open access to the route. This can be seen in the following route.

mymodule_display_page:
  path: '/test-page'
  defaults:
    _controller: '\Drupal\mymodule\Controller\TestController::someAction'
  requirements:
    _access: 'TRUE'

Although not a problem in itself, it is best to explain why this has been done as some developers might add open access routes when prototyping their modules. By explicitly flagging this, the tool allows developers to catch these open routes and sort them out.

This issue can be fixed by adding some documentation to the route access.

mymodule_display_page:
  path: '/test-page'
  defaults:
    _controller: '\Drupal\mymodule\Controller\TestController::someAction'
  requirements:
    # This is a public facing route used to render links to anonymous users.
    _access: 'TRUE'

It is usually best practice to provide at least some sort of permission on routes. The following solves the coding standard problem by applying the generic "access content" permission to this route.

mymodule_display_page:
  path: '/test-page'
  defaults:
    _controller: '\Drupal\mymodule\Controller\TestController::someAction'
  requirements:
    _permission: 'access content'

Automatically Fixing Problems

The good news is that you don't technically need to fix every problem encountered by the phpcs tool, there's a tool that can do this for you. The PHP Code Beautifier and Fixer (aka phpcbf) tool can be used to run through the code and fix problems based on standards you want to adhere to.

The tool is part of the PHP_CodeSniffer project and takes exactly the same command line arguments that the "phpcs" tool does. It is used in the following way.

phpcbf --standard=Drupal,DrupalPractice --extensions=php,module,inc,install,test,profile,theme,css,info,txt,md,yml --ignore=node_modules,bower_components,vendor web/modules/custom

This will run through all of the code in the inspection directories and attempt to correct and coding standards problems. It the tool can't fix the problem then it will inform you of this in the output of the command.

For most errors (especially whitespace errors) this tool can easily correct them. You can correct 99% of problems flagged by PHP_CodeSniffer in a few seconds; which saves you lots of time. It's much better to run this tool than to spend hours correcting white space in your code. Since it comes with PHP_CodeSniffer then you already have access to it.

If you have created a phpcs.xml file to store your configuration then PHP Code Beautifier and Fixer will pick up those configuration settings as well. This means you can run the "phpcbf" command on its own and the tool will pick up your PHP_CodeSniffer settings. In this case, there is no need to pass in additional configuration options here.

Conclusion

PHP_CodeSniffer is an excellent tool for spotting problems in your Drupal codebase. As mentioned at the start, it is essential that your code passes these standards if you are going to submit it to contributed modules. This means that if you intend on contributing code to Drupal you must first pass that code through these standard checks.

If you are running a Drupal project then it is important that you only test the code you or your team have written. There is no need to check all contributed code when running your checks as it should be compliant to the required standards already.

The good thing about PHP_CodeSniffer is that the code you inspect doesn't need to be in Drupal at all. If you install the tool as a global dependency you can run an analysis on any stand alone directory. If you are learning about the tool then running it against single files or stand alone directories is a good way to get to grips with some of the errors being produced, and how to solve them.

PHP_CodeSniffer only looks at the coding standards for PHP files (and some other files like yml). You should also be using PHPStan in Drupal projects for doing a deeper dive into coding problems. Other tools like eslint can also be used to inspect JavaScript files, which you should also be doing.

Add new comment

The content of this field is kept private and will not be shown publicly.