|
| 1 | +## Table of contents |
| 2 | + |
| 3 | +* [A Project ruleset or a standard ?](#a-project-ruleset-or-a-standard-) |
| 4 | +* [About standards](#about-standards) |
| 5 | +* [Creating an external standard for PHP_CodeSniffer](#creating-an-external-standard-for-php_codesniffer) |
| 6 | +* [Creating new rules](#creating-new-rules) |
| 7 | + * [Naming conventions](#naming-conventions) |
| 8 | + |
| 9 | +*** |
| 10 | + |
| 11 | +## A Project ruleset or a standard ? |
| 12 | + |
| 13 | +The terminology used for coding standards in the context of PHP_CodeSniffer can be confusing. |
| 14 | +When do you use a project ruleset ? When should you use a standard ? |
| 15 | + |
| 16 | +Let's try and clarify this. |
| 17 | + |
| 18 | +As a general rule of thumb: individual projects should use a project ruleset to document the rules enforced for the project. |
| 19 | +Project rulesets may _include_ one or more predefined standards. |
| 20 | + |
| 21 | +If a group of projects should use the same rules, you could create an external standard to define the common rules. This external standard can then be included in the project specific ruleset for each individual project. |
| 22 | + |
| 23 | +The typical differences between project-specific rulesets and standards are: |
| 24 | + |
| 25 | +| | Project specific ruleset | External standard | |
| 26 | +|-------------------------------------------------------------------------------|--------------------------|--------------------------------| |
| 27 | +| File name | `[.]phpcs.xml[.dist]` | `ruleset.xml` | |
| 28 | +| Location | Project root directory | Standard directory (see below) | |
| 29 | +| Will automatically be used when no standard is provided on the command line ? | Yes | No | |
| 30 | +| Can have custom sniffs ? | No | Yes | |
| 31 | +| Can be installed ? | No | Yes | |
| 32 | +| Reusability by other projects ? | Limited | Yes | |
| 33 | + |
| 34 | +For optimal reusability, it is in most cases a good idea for a standard to be in its own repository and to be maintained as a separate project. |
| 35 | + |
| 36 | +A `[.]phpcs.xml[.dist]` file and a `ruleset.xml` file can largely contain the same type of directives. |
| 37 | +The [Annotated ruleset](https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Annotated-Ruleset) page contains information on all the directives you can use. |
| 38 | + |
| 39 | +Keep in mind that for a _standard_ to be optimally reusable, it should not contain project specific information, such as `<file>` directives or `<exclude-patterns>`, while a project specific `[.]phpcs.xml[.dist]` ruleset file _can_ contain that information. |
| 40 | + |
| 41 | +You may also find the [Customisable Sniff Properties](https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Customisable-Sniff-Properties) page a handy reference for customisations which can be made to PHP_CodeSniffer native sniffs. |
| 42 | + |
| 43 | +<p align="right"><a href="#table-of-contents">back to top</a></p> |
| 44 | + |
| 45 | + |
| 46 | +## About standards |
| 47 | + |
| 48 | +In the context of PHP_CodeSniffer, a _"standard"_ is a predefined collection of rules for code to follow. |
| 49 | +Examples of standards are: PSR2 and PSR12, but a standard can also be eco-system specific, such as a Joomla or Doctrine standard, company specific or specific to a group of (related) projects. |
| 50 | + |
| 51 | +PHP_CodeSniffer has a number of build-in standards. However, if you want to define your own standard, you can. |
| 52 | +In terms of PHP_CodeSniffer, this is regarded as an _"external standard"_ and the path to the standard can be registered with PHP_CodeSniffer using the `--config-set installed_paths path/to/standard` command. |
| 53 | + |
| 54 | +A standard _may_ contain sniffs, but it doesn't have to. More about this below. |
| 55 | + |
| 56 | +<p align="right"><a href="#table-of-contents">back to top</a></p> |
| 57 | + |
| 58 | + |
| 59 | +## Creating an external standard for PHP_CodeSniffer |
| 60 | + |
| 61 | +The first thing to do is to think of a name for the standard. |
| 62 | + |
| 63 | +Rules to keep in mind: |
| 64 | +* The standard name CANNOT contain spaces, `.` characters or slashes. |
| 65 | +* The standard name CANNOT be named "Internal". |
| 66 | +* IF the standard will contain custom sniffs, the standard name MUST be a name which can be used in a PHP namespace, i.e. it is not allowed to start with a number, not allowed to contain dashes etc. |
| 67 | + |
| 68 | +Once you have a good name: |
| 69 | +* Create a directory with that name (case-sensitive). |
| 70 | +* Create a file in that directory called `ruleset.xml` with the following contents and replace `StandardName` in the below code snippet with your chosen name. |
| 71 | +```xml |
| 72 | +<?xml version="1.0"?> |
| 73 | +<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="StandardName" xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/PHPCSStandards/PHP_CodeSniffer/master/phpcs.xsd"> |
| 74 | + |
| 75 | +</ruleset> |
| 76 | +``` |
| 77 | + |
| 78 | +If your chosen standard name is `MyCompanyStandard`, the directory structure would now look like this: |
| 79 | +``` |
| 80 | +- MyCompanyStandard (directory) |
| 81 | + - ruleset.xml (file) |
| 82 | +``` |
| 83 | + |
| 84 | +Now you can start adding rules to the standard. |
| 85 | + |
| 86 | +For example, if your standard wants to enforce space indentation, you can add the `Generic.WhiteSpace.DisallowTabIndent` sniff to your ruleset: |
| 87 | +```xml |
| 88 | +<?xml version="1.0"?> |
| 89 | +<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="YourStandardName" xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/PHPCSStandards/PHP_CodeSniffer/master/phpcs.xsd"> |
| 90 | + |
| 91 | + <rule ref="Generic.WhiteSpace.DisallowTabIndent"/> |
| 92 | + |
| 93 | +</ruleset> |
| 94 | +``` |
| 95 | + |
| 96 | +As mentioned before, have a look at the [Annotated ruleset](https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Annotated-Ruleset) and the [Customisable Sniff Properties](https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Customisable-Sniff-Properties) pages for inspiration on what directives and customisations you can add to your ruleset. |
| 97 | + |
| 98 | +<p align="right"><a href="#table-of-contents">back to top</a></p> |
| 99 | + |
| 100 | + |
| 101 | +## Creating new rules |
| 102 | + |
| 103 | +There may be rules you want to enforce for which you cannot find an existing sniff, either in the PHP_CodeSniffer build-in standards or in any of the available generic external standards. |
| 104 | + |
| 105 | +In that case, you can write your own sniff to enforce that rule. |
| 106 | + |
| 107 | +> If the sniff could be generically usable, please be a good open source citizen and consider opening an issue in PHP_CodeSniffer, or in one of the external standards repositories, and offer to contribute the sniff, making it available for others to use as well. |
| 108 | +
|
| 109 | +> [!IMPORTANT] |
| 110 | +> All sniffs in a standard are automatically included. There is no need to include the sniff(s) in the `ruleset.xml` via a `<rule ref=.../>`. |
| 111 | +
|
| 112 | + |
| 113 | +There is a [Coding Standard Tutorial](https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Coding-Standard-Tutorial) available on how to write a sniff. |
| 114 | + |
| 115 | +<p align="right"><a href="#table-of-contents">back to top</a></p> |
| 116 | + |
| 117 | + |
| 118 | +### Naming conventions |
| 119 | + |
| 120 | +Sniffs need to follow strict directory layout and naming conventions to allow for PHP_CodeSniffer to autoload sniff files correctly, translate between sniff codes and sniff class names, and to set any customisations indicated in a ruleset for a particular sniff. |
| 121 | + |
| 122 | +#### 1. Directory structure |
| 123 | + |
| 124 | +A sniff MUST belong to a standard and MUST be in a category. |
| 125 | + |
| 126 | +The directory structure MUST be as follows: |
| 127 | +``` |
| 128 | +[StandardName]/Sniffs/[CategoryName]/ |
| 129 | +``` |
| 130 | + |
| 131 | +* The `[StandardName]` directory MAY be nested deeper inside a project. |
| 132 | +* No directories should exist under the `[CategoryName]` directory. |
| 133 | + |
| 134 | +#### 2. Sniff file name |
| 135 | + |
| 136 | +All sniff files MUST end on `Sniff.php` and be located within a `[CategoryName]` directory. |
| 137 | + |
| 138 | +All sniffs MUST have a name, so a sniff class called just and only `Sniff` is not allowed. |
| 139 | + |
| 140 | +Both the sniff name and the category name MUST be valid symbol names in PHP. |
| 141 | + |
| 142 | + |
| 143 | +#### 3. Namespace and class name |
| 144 | + |
| 145 | +The namespace and class name MUST follow [PSR-4](https://www.php-fig.org/psr/psr-4/). |
| 146 | + |
| 147 | +This means that - taking the example directory structure above in to account - the namespace name MUST end on `[StandardName]\Sniffs\[CategoryName]` and the class name MUST be exactly the same as the file name (minus the `.php` file extension). |
| 148 | + |
| 149 | +> [!NOTE] |
| 150 | +> As long as an external standard is registered with PHP_CodeSniffer via `installed_paths` and the standard follows the directory layout and naming conventions, PHP_CodeSniffer can, and will, automatically handle the sniff autoloading. |
| 151 | +> |
| 152 | +> As the PHP_CodeSniffer autoloader is essential for the translation between sniff codes (as used in rulesets) and sniff class names, it is strongly discouraged to set up autoloading of sniff classes via Composer, as this can interfere with the _error code to sniff class_ translation. |
| 153 | +
|
| 154 | + |
| 155 | +#### Examples |
| 156 | + |
| 157 | + |
| 158 | +##### Valid: |
| 159 | +``` |
| 160 | +<?php |
| 161 | +// File: MyStandard/Sniffs/Operators/OperatorSpacingSniff.php |
| 162 | +
|
| 163 | +namespace MyStandard\Sniffs\Operators; |
| 164 | +
|
| 165 | +use PHP_CodeSniffer\Files\File; |
| 166 | +use PHP_CodeSniffer\Sniffs\Sniff; |
| 167 | +
|
| 168 | +class OperatorSpacingSniff implements Sniff {...} |
| 169 | +``` |
| 170 | + |
| 171 | +Prefixing the namespace is allowed: |
| 172 | +``` |
| 173 | +<?php |
| 174 | +// File: MyStandard/Sniffs/Operators/OperatorSpacingSniff.php |
| 175 | +
|
| 176 | +namespace NS\Prefix\MyStandard\Sniffs\Operators; |
| 177 | +
|
| 178 | +use PHP_CodeSniffer\Files\File; |
| 179 | +use PHP_CodeSniffer\Sniffs\Sniff; |
| 180 | +
|
| 181 | +class OperatorSpacingSniff implements Sniff {...} |
| 182 | +``` |
| 183 | + |
| 184 | +Be sure to inform PHP_CodeSniffer about the namespace prefix by annotating it in the `ruleset.xml` file in the `namespace` attribute on the `ruleset` node: |
| 185 | +```xml |
| 186 | +<?xml version="1.0"?> |
| 187 | +<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="MyStandard" namespace="NS\Prefix\MyStandard" xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/PHPCSStandards/PHP_CodeSniffer/master/phpcs.xsd"> |
| 188 | + |
| 189 | +</ruleset> |
| 190 | +``` |
| 191 | + |
| 192 | +Nesting the standard directory deeper inside a project is allowed: |
| 193 | +``` |
| 194 | +<?php |
| 195 | +// File: src/Tools/PHPCS/MyStandard/Sniffs/Operators/OperatorSpacingSniff.php |
| 196 | +
|
| 197 | +namespace Vendor\Tools\PHPCS\MyStandard\Sniffs\Operators; |
| 198 | +
|
| 199 | +use PHP_CodeSniffer\Files\File; |
| 200 | +use PHP_CodeSniffer\Sniffs\Sniff; |
| 201 | +
|
| 202 | +class OperatorSpacingSniff implements Sniff {...} |
| 203 | +``` |
| 204 | +The same note about setting the `namespace` attribute in the `ruleset.xml` file applies. |
| 205 | + |
| 206 | +Also make sure that the `installed_paths` configuration option is set correctly and points to the `MyStandard` directory. |
| 207 | + |
| 208 | + |
| 209 | +##### Invalid: |
| 210 | + |
| 211 | +Sniff name not ending on `Sniff`: |
| 212 | +``` |
| 213 | +<?php |
| 214 | +// File: MyStandard/Sniffs/Operators/OperatorSpacing.php |
| 215 | +
|
| 216 | +namespace MyStandard\Sniffs\Operators; |
| 217 | +
|
| 218 | +use PHP_CodeSniffer\Files\File; |
| 219 | +use PHP_CodeSniffer\Sniffs\Sniff; |
| 220 | +
|
| 221 | +class OperatorSpacing implements Sniff {...} |
| 222 | +``` |
| 223 | + |
| 224 | +Sniff not placed in a (sub-)directory under a `Sniffs` directory: |
| 225 | +``` |
| 226 | +<?php |
| 227 | +// File: MyStandard/Operators/OperatorSpacing.php |
| 228 | +
|
| 229 | +namespace MyStandard\Operators; |
| 230 | +
|
| 231 | +use PHP_CodeSniffer\Files\File; |
| 232 | +use PHP_CodeSniffer\Sniffs\Sniff; |
| 233 | +
|
| 234 | +class OperatorSpacingSniff implements Sniff {...} |
| 235 | +``` |
| 236 | + |
| 237 | +Not following the required directory structure (missing `[CategoryName]` subdirectory): |
| 238 | +``` |
| 239 | +<?php |
| 240 | +// File: MyStandard/Sniffs/OperatorSpacingSniff.php |
| 241 | +
|
| 242 | +namespace MyStandard\Sniffs; |
| 243 | +
|
| 244 | +use PHP_CodeSniffer\Files\File; |
| 245 | +use PHP_CodeSniffer\Sniffs\Sniff; |
| 246 | +
|
| 247 | +class OperatorSpacingSniff implements Sniff {...} |
| 248 | +``` |
| 249 | + |
| 250 | +Not following the required directory structure (missing `[StandardName]` top-level directory): |
| 251 | +``` |
| 252 | +<?php |
| 253 | +// File: src/Sniffs/Operators/OperatorSpacingSniff.php |
| 254 | +
|
| 255 | +namespace Sniffs\Operators; |
| 256 | +
|
| 257 | +use PHP_CodeSniffer\Files\File; |
| 258 | +use PHP_CodeSniffer\Sniffs\Sniff; |
| 259 | +
|
| 260 | +class OperatorSpacingSniff implements Sniff {...} |
| 261 | +``` |
| 262 | + |
| 263 | +<p align="right"><a href="#table-of-contents">back to top</a></p> |
0 commit comments