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
- Registering Token Types:
Theregister()
method tells PHP CodeSniffer which token types the sniff should look for. In this case, we’re interested inT_REQUIRE
,T_REQUIRE_ONCE
,T_INCLUDE
, andT_INCLUDE_ONCE
tokens, which represent the four types of file inclusion in PHP. - Processing Tokens:
Theprocess()
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:
ThesniffPaths
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.