How to Fix a Error When Using a Custom PHP CodeSniffer Sniff

When working on a PHP project, coding standards play a crucial role in ensuring that the code is readable, maintainable, and consistent. One such standard is ensuring that all include, require, include_once, and require_once statements use the __DIR__ constant for path resolution instead of relative paths.

In my project, I couldn’t find an existing sniff to enforce this rule, so I decided to write my own custom sniff. However, when running PHP CodeSniffer, I encountered an error:

ERROR: Referenced sniff "MyCustom.IncludeUsingDirSniff" does not exist

I’ll walk you through how I created the custom sniff, set up the ruleset, and eventually solved this issue. Let’s dive in!

Creating the Custom Sniff

I started by creating a custom sniff to enforce the use of __DIR__ for path resolution in require, require_once, include, and include_once statements. Since I wanted to keep my custom sniffs separate from the production code, I created a folder named _dev where I placed my sniff file. I named it IncludeUsingDirSniff.php and used the namespace MyCustom.

Here’s the complete code for the custom sniff:

<?php declare(strict_types=1);

namespace MyCustom;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;

/**
* Checks that all require/require_once/include/include_once statements use __DIR__ for file paths.
*
* Correct usage:
* require_once __DIR__ . '/../settings.php';
*/
class IncludeUsingDirSniff implements Sniff
{
/**
* Returns an array of token types that this sniff is interested in.
*
* @return int[]
*/
public function register(): array
{
return [
T_REQUIRE,
T_REQUIRE_ONCE,
T_INCLUDE,
T_INCLUDE_ONCE,
];
}

/**
* Processes the token.
*
* Iterates over all tokens of the statement to check if "__DIR__" is present.
*
* @param File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the token in the token stack.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr): void
{
$tokens = $phpcsFile->getTokens();

// Determine the end of the current statement (typically until the semicolon)
$end = $phpcsFile->findEndOfStatement($stackPtr);
if ($end === false) {
$end = count($tokens) - 1;
}

$foundDir = false;
for ($i = $stackPtr; $i <= $end; $i++) {
if (strpos($tokens[$i]['content'], '__DIR__') !== false) {
$foundDir = true;
break;
}
}

if (!$foundDir) {
$error = 'The %s statement must use __DIR__ for the file path.';
$phpcsFile->addError(sprintf($error, $tokens[$stackPtr]['content']), $stackPtr, 'MissingDirUsage');
}
}
}

Explanation of the Sniff

  1. Registering Token Types:
    The register() method tells PHP CodeSniffer which token types the sniff should look for. In this case, we’re interested in T_REQUIRE, T_REQUIRE_ONCE, T_INCLUDE, and T_INCLUDE_ONCE tokens, which represent the four types of file inclusion in PHP.
  2. Processing Tokens:
    The process() method iterates through the tokens in the PHP file. It checks whether the __DIR__ constant is present in the include/require statement. If not, an error is added to the file using $phpcsFile->addError().

Setting Up the Ruleset

Once I created the custom sniff, the next step was to configure PHP CodeSniffer to use it. To do this, I created a phpcs.xml file in the project root, which explicitly loads my custom sniff file.

Here’s how the phpcs.xml file looked:

<?xml version="1.0"?>
<ruleset name="Custom Coding Standard">
<description>
My Stuff
</description>

<!-- the important 2 lines -->
<config name="sniffPaths" value="/var/www/buero/_dev" />
<file name="/var/www/buero/_dev/IncludeUsingDirSniff.php"/>

<rule ref="PSR12">
<exclude name="Generic.WhiteSpace.DisallowTabIndent"/>
<exclude name="Generic.ControlStructures.InlineControlStructure"/>
</rule>
<arg name="tab-width" value="4"/>

<rule ref="Generic.PHP.ForbiddenFunctions">
<properties>
<property name="forbiddenFunctions" type="array">
<element key="var_dump" value="true"/>
<element key="print_r" value="true"/>
<element key="echo" value="true"/>
</property>
</properties>
</rule>
</ruleset>

Key Points:

  • sniffPaths Configuration:
    The sniffPaths attribute tells PHP CodeSniffer where to look for custom sniff files. In my case, it was the _dev directory.
  • file Tag:
    The <file> tag explicitly points to the path of the custom sniff file, ensuring that it gets loaded.

Running PHP CodeSniffer

With the sniff and ruleset in place, I was ready to run PHP CodeSniffer. However, when I executed the following command:

$phpcs --standard=_dev/phpcs.xml /var/www/buero

I encountered the following error:

ERROR: Referenced sniff "MyCustom.IncludeUsingDirSniff" does not exist

What Went Wrong?

After some troubleshooting, I realized that PHP CodeSniffer couldn’t find the custom sniff because the sniffPaths configuration and the <file> tag were not correctly aligned. The paths I provided in the XML file were not absolute, and PHP CodeSniffer couldn’t resolve the relative path to my custom sniff.

How I Fixed It:

I updated the phpcs.xml file to reference the correct file paths for the sniff:

<config name="sniffPaths" value="/var/www/buero/_dev"/>
<file name="/var/www/buero/_dev/IncludeUsingDirSniff.php"/>

This way, PHP CodeSniffer could find and load my custom sniff, and the error disappeared.

Final Thoughts

Creating a custom sniff to enforce coding standards is an excellent way to ensure consistency in a project. Although I initially faced issues with the configuration and file paths, I was able to resolve them by double-checking the paths in my ruleset and ensuring they matched the structure of my project.

By writing this custom sniff, I could enforce the use of __DIR__ for all include/require statements in the code, which will improve maintainability and ensure that file paths are always resolved relative to the current script directory.

Related blog posts