skip to content

In a PHP_CodeSniffer phpcs.xml.dist ruleset, how do rule ref, exclude and properties decide which sniffs run and how they behave?

level: middleimportance: must knowfreq 50%

answer

  1. found in the working directory or above
  2. ref a standard, a sniff or one code
  3. exclude by sniff or message code
  4. public sniff property set via property
  5. array values use element tags in 4.0

basics

~20 s

A phpcs.xml.dist ruleset pulls in a whole standard, a category, one sniff or one message with rule ref, removes parts of it with exclude, and tunes a sniff's public settings with properties, so the whole team runs the same checks.

solid answer

~30 s

When no `--standard` is given, `phpcs` looks for `.phpcs.xml`, `phpcs.xml`, `.phpcs.xml.dist` or `phpcs.xml.dist`, in that order, in the working directory and its parents. Inside it, `<rule ref="PSR12"/>` includes a whole standard, `<rule ref="Generic.PHP.ForbiddenFunctions"/>` one sniff, and a four-part ref a single message. `<exclude name="…"/>` inside a rule removes a sniff, or just one message code. `<properties><property name="lineLimit" value="100"/></properties>` sets a sniff's public property; arrays use `<element key="…" value="…"/>`, and `extend="true"` adds to the default instead of replacing it. `<file>`, `<exclude-pattern>` and `<arg>` pin paths and CLI options, so a bare `vendor/bin/phpcs` runs the team standard.

code

xml · 24 lines
xml
<?xml version="1.0"?>
<ruleset name="Acme">
    <file>src</file>
    <file>tests</file>
    <exclude-pattern>*/var/cache/*</exclude-pattern>
    <arg name="extensions" value="php"/>
    <arg value="sp"/>

    <rule ref="PSR12">
        <exclude name="PSR12.Files.FileHeader.SpacingAfterUseBlock"/>
    </rule>
    <rule ref="Generic.Files.LineLength">
        <properties>
            <property name="lineLimit" value="100"/>
        </properties>
    </rule>
    <rule ref="Generic.PHP.ForbiddenFunctions">
        <properties>
            <property name="forbiddenFunctions" type="array" extend="true">
                <element key="var_dump" value="null"/>
            </property>
        </properties>
    </rule>
</ruleset>

go deeper

for a junior

Recall that phpcs.xml.dist is the committed ruleset, that rule ref includes a standard or sniff, and that exclude removes one.

for a middle

Explain the file lookup order, how the number of dot-separated parts changes what a ref or exclude targets, and how properties tune a sniff.

for a senior

Show you can make the ruleset self-contained with file, arg and exclude-pattern so every run is identical, and migrate 3.x array properties.

for a principal

Weigh one shared base standard referenced by many repositories against per-repository rulesets, and who owns changes to it.

## Where the ruleset comes from A **ruleset** is an XML file that tells PHP_CodeSniffer which **sniffs** (individual checks) to run, how to configure them, and which files to scan. When you run `phpcs` or `phpcbf` without `--standard`, the tool searches the current directory, then each parent directory, for the first of these names, in this order of precedence: 1. `.phpcs.xml` 2. `phpcs.xml` 3. `.phpcs.xml.dist` 4. `phpcs.xml.dist` The usual convention is to **commit `phpcs.xml.dist`** as the team standard and let a developer create an uncommitted `phpcs.xml` for local experiments. The local file **replaces** the `.dist` one rather than merging with it; to build on the team file, the local one includes it with `<rule ref="./phpcs.xml.dist"/>`. If no file is found and no `--standard` is passed, PHP_CodeSniffer 4.0 falls back to the configured `default_standard`, or to `PSR12` (3.x used `PEAR`). ## Including rules with rule ref `<rule ref="...">` accepts references at several granularities, and the number of dot-separated parts tells PHP_CodeSniffer which one you mean: | Reference | Parts | What it includes | |---|---|---| | `PSR12` | 1 | the whole standard, including standards it pulls in | | `Squiz.Commenting` | 2 | every sniff in one category | | `Generic.PHP.ForbiddenFunctions` | 3 | one sniff with all its messages | | `Generic.Files.LineLength.TooLong` | 4 | one message code of a sniff | | `./tools/Acme` or `./base.xml` | path | another standard directory or ruleset file | A four-part reference to a sniff that is not otherwise included turns on **only that message**; every other message of that sniff stays off. ## Removing rules with exclude Inside a `<rule>`, `<exclude name="..."/>` subtracts from what that reference brought in: - a three-part name such as `PSR12.Files.FileHeader` removes the **whole sniff**; - a four-part name such as `PSR12.Files.FileHeader.SpacingAfterUseBlock` switches off **one message**, which PHP_CodeSniffer implements by setting that code's severity to 0. Run `phpcs -s` to see the full code of every message before excluding it, and `phpcs -e` to list every sniff a standard contains. Rules can also be scoped to paths: `<exclude-pattern>` inside a `<rule>` skips that rule for matching files, while a top-level `<exclude-pattern>` skips the files entirely. ## Configuring sniffs with properties Many sniffs expose **public properties**, and `<properties>` sets them per project: - scalar: `<property name="lineLimit" value="100"/>` on `Generic.Files.LineLength`; - array: `<property name="forbiddenFunctions" type="array">` with child `<element key="var_dump" value="null"/>` tags; - `extend="true"` on an array property **adds** to the default or inherited value instead of replacing it. PHP_CodeSniffer 4.0 removed the old comma-separated string syntax for array properties (`print=>echo,...`), so a 3.x ruleset using it must switch to `<element>` tags. 4.0 also casts `true`, `false` and `null` values consistently. Setting a property the sniff does not declare is a ruleset error that stops the run. The same `<rule>` can also carry `<severity>`, `<type>` (turn an error into a warning or back) and `<message>` to rewrite the text. ## Pinning the run Top-level elements make the ruleset self-contained, which is what lets a pull request check run with no flags: - `<file>src</file>` and `<file>tests</file>` set the paths to scan; - `<arg name="extensions" value="php"/>`, `<arg name="parallel" value="8"/>` and `<arg value="sp"/>` (short flags `-s` and `-p`) store command-line options; - `<config name="..." value="..."/>` sets config values for this run. In 4.0, `<arg>` and `<config>` in the root ruleset always win over the same directive in an included ruleset. ## Common ruleset mistakes - **Excluding too broadly.** Removing a whole sniff to silence one message also loses its other checks; exclude the four-part message code instead. - **Forgetting paths.** A ruleset without `<file>` makes every invocation pass paths by hand, and different people pass different ones. - **Typos in property names.** A property the sniff does not declare is reported as a ruleset error and stops the run, which is the tool protecting you from a setting that silently did nothing. - **Assuming merge semantics.** Two discovered files are never combined; composition happens only through `<rule ref>` pointing at another standard or ruleset file. - **Scoping with annotations.** Skipping generated or legacy directories belongs in `<exclude-pattern>`, not in comments sprinkled through the code. ## Why this matters for a team With a committed `phpcs.xml.dist`, `vendor/bin/phpcs` on a laptop and the check on a pull request read the same standard, the same exclusions and the same paths. Differences then come from the code, not from who typed which flags.

  • A developer adds a local phpcs.xml and suddenly half the team rules stop running for them. Why?
    `phpcs.xml` outranks `phpcs.xml.dist` in the lookup order, and PHP_CodeSniffer uses only the first file it finds; the two are not merged. The local file must include the team file with `<rule ref="./phpcs.xml.dist"/>` and then add its own tweaks, otherwise it replaces the committed standard entirely.
  • What is the difference between excluding PSR12.Files.FileHeader and PSR12.Files.FileHeader.SpacingAfterUseBlock?
    The three-part name removes the whole FileHeader sniff, so none of its messages are reported. The four-part name keeps the sniff running and switches off just that one message code by setting its severity to 0. Excluding by message code is the narrower, safer choice when only one check is unwanted.
  • Why might a 3.x ruleset's array property stop working under PHP_CodeSniffer 4.0?
    4.0 removed the old comma-delimited string form such as `print=>echo,create_function=>null`. Array properties must now use `type="array"` with one `<element key="..." value="..."/>` per entry, optionally with `extend="true"` to add to the sniff's default list.

saying these in an interview costs you the question

  • A local phpcs.xml is merged with the committed phpcs.xml.dist
  • Excluding a message code removes the whole sniff
  • Sniff settings can only be passed as command-line flags
  • Array properties still take a comma-separated string in 4.0
  • Without a ruleset file phpcs 4.0 checks against PEAR