skip to content

Your team enforces a phpcs.xml.dist ruleset on every pull request; what can break when upgrading PHP_CodeSniffer from 3.x to 4.0?

level: seniorimportance: nice to knowfreq 15%

answer

  1. exit codes redefined
  2. PEAR to PSR12 default
  3. output split to STDERR
  4. @codingStandardsIgnore removed
  5. renamed and removed sniff codes

basics

~20 s

Upgrading to PHP_CodeSniffer 4.0 changes exit codes, the default standard (PEAR to PSR12) and output streams, removes the @codingStandardsIgnore annotations, some sniffs and the string array-property syntax, and renames several error codes, so check scripts, suppressions and the ruleset.

solid answer

~40 s

Start with the **pipeline**: in 4.0 `phpcs` exits 1 for only fixable, 2 for only non-fixable and 3 for both, and `phpcbf` exits 0 when nothing remains; 3.x returned 1 after a successful fix. Progress and status now go to STDERR, so a redirected report contains only report output. Next, the **ruleset**: a reference to a removed sniff such as `Generic.Formatting.NoSpaceAfterCast` aborts with "does not exist"; a renamed code such as `Squiz.Classes.ValidClassName.NotCamelCaps`, now `NotPascalCase`, quietly stops matching its exclude. Comma-string array properties must become `<element>` tags. Then the **code**: `@codingStandardsIgnore*` comments are gone, so suppressed violations resurface. Runs with no ruleset now use PSR12, CSS and JS checking is removed, and custom sniffs must implement the `Sniff` interface.

code

xml · 16 lines
xml
<!-- 3.x ruleset fragments that need attention under 4.0 -->
<rule ref="Generic.Formatting.NoSpaceAfterCast"/>               <!-- removed: run aborts -->

<rule ref="Squiz.Classes.ValidClassName">
    <exclude name="Squiz.Classes.ValidClassName.NotCamelCaps"/> <!-- renamed: NotPascalCase -->
</rule>

<!-- 4.0 replacements -->
<rule ref="Generic.Formatting.SpaceAfterCast">
    <properties>
        <property name="spacing" value="0"/>
    </properties>
</rule>
<rule ref="Squiz.Classes.ValidClassName">
    <exclude name="Squiz.Classes.ValidClassName.NotPascalCase"/>
</rule>

go deeper

for a junior

Recall that 4.0 is a breaking release: new exit codes, PSR12 as default standard, and the old @codingStandardsIgnore comments no longer work.

for a middle

Explain which ruleset changes abort a run (removed sniffs) and which fail quietly (renamed codes), and how array properties must be rewritten.

for a senior

Show a staged upgrade: diff 3.x and 4.0 reports with -s, fix ruleset references, rewrite suppressions, and re-verify exit-code handling in scripts.

for a principal

Plan tool upgrades as their own changes with a comparison report, so standard changes and tool changes never land in the same merge.

## Why the upgrade needs a plan PHP_CodeSniffer 4.0 (released 2025-09-16, current 4.0.4) is a major release with deliberate breaking changes. The project published separate upgrade guides for ruleset users and for sniff developers. For a team that runs `phpcs` against a committed `phpcs.xml.dist` on every pull request, the breakage shows up in three places: the **pipeline** that interprets results, the **ruleset** itself, and the **code** that carries suppressions. It also requires PHP 7.2 or later to run. ## The pipeline: exit codes and output streams | Situation | 3.x | 4.0 | |---|---|---| | `phpcs` found nothing | 0 | 0 | | `phpcs` found only fixable violations | 2 | 1 | | `phpcs` found only non-fixable violations | 1 | 2 | | `phpcs` found both | 2 | 3 | | `phpcbf` fixed everything, nothing left | 1 | 0 | | `phpcbf` could not fix a file (fixer conflict) | 2 | the 4 bit is set (5 or 7) | | processing error (bad flag, broken ruleset) | 3 | 16 | A script that treated `phpcbf`'s 1 as success, or that branched on a specific `phpcs` code, now misreads the result. A check that only tests for non-zero keeps working. 4.0 also adds `ignore_non_auto_fixable_on_exit` beside `ignore_errors_on_exit` and `ignore_warnings_on_exit`. Status, progress and debug output now go to **STDERR**; only the report goes to **STDOUT**. A job that captured `phpcs > report.txt` now gets a clean report, and anything that parsed progress lines from STDOUT sees none. `--report-file` is unchanged. ## The ruleset: removed, renamed and reinterpreted 1. **Removed sniffs abort the run.** A `<rule ref>` to a sniff removed in 4.0 produces `Referenced sniff "..." does not exist` and the run stops. Examples: `Generic.Formatting.NoSpaceAfterCast` (use `Generic.Formatting.SpaceAfterCast` with `spacing` 0), `Squiz.WhiteSpace.LanguageConstructSpacing` (use the Generic one), `Generic.Functions.CallTimePassByReference`, the whole `MySource` standard, and every CSS or JS sniff. 2. **Renamed codes fail quietly.** `Squiz.Classes.ValidClassName.NotCamelCaps` became `NotPascalCase`. `Squiz.PHP.Heredoc.NotAllowed` split into `HeredocNotAllowed` and `NowdocNotAllowed`. `PSR12.Files.FileHeader.SpacingAfterBlock` split into per-block codes. An `<exclude>` or `<severity>` on the old name matches nothing, so the message comes back. 3. **Array properties** set as a comma-separated string (`print=>echo,...`) no longer work; use `type="array"` with `<element key="..." value="..."/>`. 4. **`error` properties removed.** `Generic.Strings.UnnecessaryStringConcat` now always raises errors, and `Generic.Formatting.MultipleStatementAlignment` always raises warnings; change them with `<type>` in the rule. 5. **Precedence changed.** `<arg>` and `<config>` in the root ruleset now always win over the same directive in an included ruleset. 6. **No ruleset, new default.** A run with neither `--standard` nor a `phpcs.xml*` file now checks `PSR12`, not `PEAR`. ## The code: suppressions and parse errors - The `@codingStandardsIgnoreLine`, `...Start`, `...End` and `...File` comments were removed. Every violation they hid is reported again until they are rewritten as `phpcs:ignore`, `phpcs:disable`, `phpcs:enable` or `phpcs:ignoreFile`. - Built-in sniffs no longer warn about possible parse errors; add `Generic.PHP.Syntax` if the check relied on them. - Files without an extension are scanned when passed explicitly, and `--extensions` no longer accepts a `/php` tokenizer suffix. ## Custom sniffs In-house sniffs must implement `PHP_CodeSniffer\Sniffs\Sniff`, follow the `Standard\Sniffs\Category\NameSniff` naming convention, and handle namespaced names as `T_NAME_QUALIFIED` or `T_NAME_FULLY_QUALIFIED` tokens. Their tests extend `AbstractSniffTestCase` instead of `AbstractSniffUnitTest`. ## What does not change Not everything moves. The ruleset file names and their lookup order, the `phpcs:` annotation syntax, the sniff code format, `--report-file` and the `full` report layout all stay the same, and most built-in sniffs keep their codes. That is why a side-by-side report comparison is effective: most of the output is identical, and the differences point straight at what needs attention. ## A safe upgrade sequence 1. On a branch, run 4.0 with `-s` and compare its report with the 3.x report on the same commit. 2. Fix every "does not exist" error, then search the ruleset for renamed codes. 3. Rewrite `@codingStandardsIgnore*` comments, then update exit-code handling in scripts. 4. Merge the upgrade on its own, separate from any change to the standard.

  • Why does a renamed error code cause more trouble than a removed sniff?
    A reference to a removed sniff fails loudly: the run stops with a "does not exist" error, so someone must fix it. An `<exclude>` for a renamed code, such as `NotCamelCaps` now `NotPascalCase`, points at a code that is never raised, so it matches nothing and raises no complaint. The message it used to hide simply starts failing pull requests.
  • How do you confirm that a CI step still interprets PHP_CodeSniffer 4.0 results correctly?
    Run `phpcs` against a fixture that has only fixable violations, one with only manual violations and one clean file, and check the step reports 1, 2 and 0. For `phpcbf`, a fully fixable file should now end with 0. Checks that only test for non-zero are unaffected.

saying these in an interview costs you the question

  • 4.0 only adds sniffs, so a 3.x ruleset works unchanged
  • phpcbf still exits 1 after a successful fix in 4.0
  • An exclude on a renamed code raises an error that points to the new name
  • @codingStandardsIgnoreLine comments keep suppressing after the upgrade
  • Progress output still arrives on STDOUT in 4.0