Skip to main content

PHP Quality Tools - PHPCS & PHPStan

PHP Quality Tools - PHPCS & PHPStan

PHP_CodeSniffer (PHPCS) and PHPStan are essential quality tools for Drupal module development. They enforce coding standards and catch type errors before runtime.


PHP_CodeSniffer (PHPCS)

Overview

PHPCS detects violations of coding standards in PHP code. Required for all Drupal modules.

Installation

# Via Composer (project-level) composer require --dev drupal/coder dealerdirect/phpcodesniffer-composer-installer # Verify installation ./vendor/bin/phpcs --version # Register Drupal standards ./vendor/bin/phpcs -i # Installed standards: Drupal, DrupalPractice, MySource, PEAR, PSR1, PSR2, PSR12, Squiz, Zend

Configuration

phpcs.xml

<?xml version="1.0"?> <ruleset name="Drupal Module Standards"> <description>PHPCS configuration for Drupal modules</description> <!-- Use Drupal coding standards --> <rule ref="Drupal"/> <rule ref="DrupalPractice"/> <!-- Scan these directories --> <file>modules/custom</file> <file>themes/custom</file> <!-- Exclude patterns --> <exclude-pattern>*/node_modules/*</exclude-pattern> <exclude-pattern>*/vendor/*</exclude-pattern> <exclude-pattern>*/contrib/*</exclude-pattern> <exclude-pattern>*.md</exclude-pattern> <!-- File extensions to check --> <arg name="extensions" value="php,module,inc,install,test,profile,theme"/> <!-- Show progress --> <arg value="p"/> <!-- Use colors --> <arg name="colors"/> <!-- Parallel processing --> <arg name="parallel" value="8"/> </ruleset>

Usage

# Check all files ./vendor/bin/phpcs # Check specific directory ./vendor/bin/phpcs modules/custom/my_module/ # Check single file ./vendor/bin/phpcs modules/custom/my_module/src/Controller/MyController.php # Show sniff codes (for exclusions) ./vendor/bin/phpcs -s modules/custom/my_module/ # Generate report in different formats ./vendor/bin/phpcs --report=summary ./vendor/bin/phpcs --report=json ./vendor/bin/phpcs --report=xml

Auto-Fixing Issues

# Automatically fix coding standard violations ./vendor/bin/phpcbf modules/custom/my_module/ # Preview changes without applying ./vendor/bin/phpcbf --dry-run modules/custom/my_module/ # Using BuildKit buildkit drupal phpcs modules/custom/my_module/ --fix

Example Output

$ ./vendor/bin/phpcs modules/custom/my_module/ FILE: modules/custom/my_module/src/Controller/UserController.php ---------------------------------------------------------------------- FOUND 8 ERRORS AND 4 WARNINGS AFFECTING 9 LINES ---------------------------------------------------------------------- 12 | ERROR | [x] Expected 1 space after IF keyword; 0 found 15 | ERROR | [ ] Missing function doc comment 23 | WARNING | [ ] Line exceeds 80 characters; contains 95 characters 28 | ERROR | [x] Expected 1 newline at end of file; 0 found 32 | ERROR | [ ] Method name "UserController::Get_User" is not in lowerCamel format 45 | WARNING | [ ] Inline comments must end in full-stops, exclamation marks, or question marks 52 | ERROR | [x] Space after opening parenthesis of function call prohibited 67 | ERROR | [ ] Missing parameter type hint 72 | ERROR | [ ] Missing return type declaration 89 | WARNING | [ ] t() calls should be avoided in classes, use \$this->t() instead 95 | ERROR | [x] TRUE, FALSE and NULL must be lowercase; expected "false" but found "FALSE" ---------------------------------------------------------------------- PHPCBF CAN FIX THE 4 MARKED SNIFF VIOLATIONS AUTOMATICALLY ---------------------------------------------------------------------- Time: 542ms; Memory: 12MB

Common PHPCS Violations

1. Function Naming

// BAD - Not in lowerCamel format public function Get_User() {} // GOOD public function getUser() {}

2. Missing Doc Comments

// BAD - Missing doc comment public function calculateTotal($items) { return array_sum($items); } // GOOD /** * Calculates the total sum of items. * * @param array $items * Array of numeric values to sum. * * @return float * The total sum. */ public function calculateTotal(array $items): float { return array_sum($items); }

3. Line Length

// BAD - Line too long (>80 characters) $result = $this->entityTypeManager->getStorage('node')->loadByProperties(['type' => 'article', 'status' => 1]); // GOOD $storage = $this->entityTypeManager->getStorage('node'); $result = $storage->loadByProperties([ 'type' => 'article', 'status' => 1, ]);

4. Translation Functions

// BAD - Using t() in class class MyController { public function build() { return ['#markup' => t('Hello World')]; } } // GOOD - Using $this->t() class MyController extends ControllerBase { public function build() { return ['#markup' => $this->t('Hello World')]; } }

PHPStan

Overview

PHPStan performs static analysis to catch bugs, type errors, and logical issues without running code.

Installation

# Via Composer composer require --dev phpstan/phpstan phpstan/extension-installer # Drupal-specific extension composer require --dev mglaman/phpstan-drupal phpstan/phpstan-deprecation-rules # Verify installation ./vendor/bin/phpstan --version

Configuration

phpstan.neon

includes: - vendor/mglaman/phpstan-drupal/extension.neon parameters: level: 8 paths: - modules/custom - themes/custom excludePaths: - */node_modules/* - */vendor/* - */tests/fixtures/* scanDirectories: - web/core - web/modules/contrib scanFiles: - web/core/modules/node/node.module drupal: drupal_root: web ignoreErrors: # Ignore specific errors - '#Access to an undefined property#' reportUnmatchedIgnoredErrors: false checkMissingIterableValueType: false

Analysis Levels

PHPStan has 10 levels (0-9):

LevelChecks
0Basic checks, unknown classes
1Unknown methods, properties
2Unknown methods on all expressions
3Return types, property types
4Dead code, always true/false conditions
5Checking types of arguments
6Missing typehints
7Checking for partially wrong union types
8Checking for missing typehints (recommended for Drupal)
9Strictest - mixed types not allowed

Drupal projects should aim for Level 8.

Usage

# Run PHPStan ./vendor/bin/phpstan analyse # Specific directory ./vendor/bin/phpstan analyse modules/custom/my_module/ # Specific level ./vendor/bin/phpstan analyse --level=6 modules/custom/my_module/ # Generate baseline (ignore existing errors) ./vendor/bin/phpstan analyse --generate-baseline # Memory limit (for large projects) ./vendor/bin/phpstan analyse --memory-limit=2G # Using BuildKit buildkit drupal phpstan modules/custom/my_module/

Example Output

$ ./vendor/bin/phpstan analyse modules/custom/my_module/ 8/8 [] 100% ------ ------------------------------------------------------------------ Line src/Controller/UserController.php ------ ------------------------------------------------------------------ 23 Parameter #1 $id of method UserController::getUser() expects int, string given. 45 Method UserController::loadUser() should return Drupal\user\UserInterface|null but returns Drupal\Core\Entity\EntityInterface|null. 67 Call to an undefined method Drupal\Core\Entity\EntityInterface::getEmail(). 89 Property UserController::$entityTypeManager is never written, only read. 112 Dead code after return statement. ------ ------------------------------------------------------------------ [ERROR] Found 5 errors

Common PHPStan Issues

1. Type Mismatches

// ERROR - Type mismatch public function getUser(int $id): User { return $this->userStorage->load($id); // Returns EntityInterface|null } // FIXED - Proper type handling public function getUser(int $id): ?UserInterface { $user = $this->userStorage->load($id); if (!$user instanceof UserInterface) { return NULL; } return $user; }

2. Missing Type Hints

// ERROR - Missing type hints public function calculateTotal($items) { return array_sum($items); } // FIXED - With type hints public function calculateTotal(array $items): float { return array_sum($items); }

3. Undefined Properties

// ERROR - Property never written class MyService { protected $entityTypeManager; // Never injected public function loadNode($id) { return $this->entityTypeManager->getStorage('node')->load($id); } } // FIXED - Dependency injection class MyService { protected EntityTypeManagerInterface $entityTypeManager; public function __construct(EntityTypeManagerInterface $entityTypeManager) { $this->entityTypeManager = $entityTypeManager; } public function loadNode(int $id): ?NodeInterface { return $this->entityTypeManager->getStorage('node')->load($id); } }

4. Dead Code

// ERROR - Dead code after return public function process(): bool { if ($this->isValid()) { return TRUE; } return FALSE; $this->log('Processing complete'); // Dead code } // FIXED - Remove dead code public function process(): bool { if ($this->isValid()) { return TRUE; } $this->log('Processing complete'); return FALSE; }

BuildKit Integration

CLI Commands

# Run PHPCS buildkit drupal phpcs modules/custom/my_module/ # Auto-fix PHPCS violations buildkit drupal phpcs modules/custom/my_module/ --fix # Run PHPStan buildkit drupal phpstan modules/custom/my_module/ # Run both buildkit drupal quality modules/custom/my_module/ # Generate reports buildkit drupal quality modules/custom/my_module/ --report

Automated Quality Checks

# Full quality gate buildkit golden audit modules/custom/my_module/ # Includes: # - PHPCS (Drupal standards) # - PHPStan (level 8) # - PHPUnit (tests) # - Security audit # - Dependency check

CI/CD Integration

GitLab CI

# .gitlab-ci.yml phpcs: stage: quality script: - composer install - ./vendor/bin/phpcs modules/custom/ allow_failure: false phpstan: stage: quality script: - composer install - ./vendor/bin/phpstan analyse modules/custom/ --level=8 allow_failure: false quality-gate: stage: quality script: - buildkit drupal quality modules/custom/ artifacts: reports: quality: quality-report.json

Pre-Commit Hook

#!/bin/bash # .git/hooks/pre-commit echo " Running PHP quality checks..." # Run PHPCS ./vendor/bin/phpcs modules/custom/ if [ $? -ne 0 ]; then echo "PHPCS failed. Run 'phpcbf' to auto-fix." exit 1 fi # Run PHPStan ./vendor/bin/phpstan analyse modules/custom/ if [ $? -ne 0 ]; then echo "PHPStan failed. Fix type errors." exit 1 fi echo "Quality checks passed" exit 0

Ignoring Specific Violations

PHPCS Ignore

// phpcs:ignore Drupal.Commenting.FunctionComment.Missing public function legacyFunction() { // Legacy code without doc comment } // Ignore entire file // phpcs:ignoreFile

PHPStan Ignore

// @phpstan-ignore-next-line $result = $this->legacyMethod(); // Ignore specific error /** @phpstan-ignore-line property.notFound */ $value = $entity->customProperty;

Best Practices

1. Run Locally Before Committing

# Quick check ./vendor/bin/phpcs modules/custom/my_module/ ./vendor/bin/phpstan analyse modules/custom/my_module/ # Auto-fix what's possible ./vendor/bin/phpcbf modules/custom/my_module/

2. Use Baseline for Legacy Code

# Generate PHPStan baseline ./vendor/bin/phpstan analyse --generate-baseline # Now only new errors will fail ./vendor/bin/phpstan analyse

3. Progressive PHPStan Levels

# Start at level 5, gradually increase parameters: level: 5 # Increase to 6, 7, 8 over time

4. IDE Integration

Configure your IDE to show PHPCS/PHPStan errors in real-time:

  • PHPStorm: Settings PHP Quality Tools PHP_CodeSniffer / PHPStan
  • VS Code: Install PHP Sniffer & AutoFixer extension