Drupal Coding Standards at CI Parity: phpcs & phpstan
When to Use
Use this when setting up local code-quality enforcement to match what the CI pipeline checks. The goal is zero surprises when phpcs or phpstan runs on your MR.
The Two phpcs Rulesets
Both rulesets ship in drupal/coder (current: v9.0.0, released Mar 2026) and run through PHP_CodeSniffer.
| Ruleset | Role | CI behavior |
|---|---|---|
Drupal |
Mandatory — formatting, naming, control structures | Blocking (allow_failure: false by default) |
DrupalPractice |
Recommended — best practices, common mistakes; may false-positive | Same job, non-blocking by default on phpstan but phpcs is blocking |
What the Drupal Standard Encodes
- Naming — functions:
module_function_name(); constants:UPPER_CASE; classes:UpperCamelCase; methods:lowerCamelCase; interfaces suffixInterface, traits suffixTrait, test classes suffixTest. - Formatting — 2-space indent, never tabs; ~80-character line target;
['key' => 'value']short array syntax; spaces around binary operators and after control keywords; single quotes by default. - Type hints — mandatory in Drupal 9+ code; return types required.
- Docblocks — every function/class/method/property/constant needs a
/** */docblock (including private); summary line under 80 chars;@param,@return,@throwswith lowercase PHPDoc types; hook implementations documented asImplements hook_name().
JS/CSS:
drupal/coderdropped JS/CSS sniffs. Use ESLint and Stylelint for those — route JS/CSS standards questions there, not tocoder.
Installing and Running Locally
# Install as dev dependency (per-project — preferred for team consistency)
composer require --dev drupal/coder squizlabs/php_codesniffer
# Register the Drupal standards
./vendor/bin/phpcs --config-set installed_paths vendor/drupal/coder/coder_sniffer
./vendor/bin/phpcs -i # confirms "Drupal" and "DrupalPractice" are listed
# Check your module
./vendor/bin/phpcs --standard=Drupal,DrupalPractice \
--extensions=php,module,inc,install,test,profile,theme,info,txt,md,yml \
path/to/module
# Auto-fix locally (LOCAL ONLY — CI never auto-fixes)
./vendor/bin/phpcbf --standard=Drupal,DrupalPractice path/to/module
The fix loop: run phpcbf → re-run phpcs → commit clean code. CI only reports violations; it never auto-fixes.
The phpcs.xml.dist Config File
A phpcs.xml.dist at the project root scopes standards enforcement to your code:
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="my_module">
<description>Drupal coding standards for my_module.</description>
<file>src</file>
<file>tests</file>
<rule ref="Drupal"/>
<rule ref="DrupalPractice"/>
<arg name="extensions" value="php,module,inc,install,test,profile,theme,yml"/>
</ruleset>
CI reads this file automatically when present. Without it, the CI job uses defaults.
PHPStan Setup
PHPStan runs with mglaman/phpstan-drupal as a Drupal API extension. The phpstan.neon at your project root:
includes:
- vendor/mglaman/phpstan-drupal/extension.neon
parameters:
level: 2
paths:
- src
- tests
PHPStan defaults to allow_failure: true in the templates — it is non-blocking unless the maintainer enforces it. Still run it locally; phpstan catches real bugs that phpcs misses.
Version matrix: PHPStan ^1.12.27 || ^2.1.54 on D11; ^1.x only on D10. mglaman/phpstan-drupal ^1.3.9 || ^2.0.15 on D11. Resolve from drupal/core-dev — do not hardcode.
DrupalPractice False Positives
DrupalPractice catches common mistakes but sometimes flags code that is intentionally written differently. Suppress a false positive with a targeted inline annotation:
// phpcs:ignore DrupalPractice.General.OptionsT9n.MissingT9n
$options = ['value' => 'Value'];
Prefer narrowing the suppression scope to the specific rule rather than // phpcs:ignore (which suppresses all rules for that line).
Common Mistakes
- Running
phpcbfand assuming it fixed everything — re-runphpcsafter auto-fix; some violations require manual resolution. - Routing JS/CSS questions to
drupal/coder— it no longer handles those; use ESLint/Stylelint. - Hardcoding PHPUnit/PHPStan version constraints in
composer.jsoninstead of resolving from the target core — CI uses the core-resolved version. - Setting
level: 9PHPStan on a new module that has no baseline — start at 2–4 and raise it; false positives at high levels are common in Drupal code. - Using a global
phpcsinstall instead of a per-project--devdependency — version skew causes different results locally vs. CI.