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):
| Level | Checks |
|---|---|
| 0 | Basic checks, unknown classes |
| 1 | Unknown methods, properties |
| 2 | Unknown methods on all expressions |
| 3 | Return types, property types |
| 4 | Dead code, always true/false conditions |
| 5 | Checking types of arguments |
| 6 | Missing typehints |
| 7 | Checking for partially wrong union types |
| 8 | Checking for missing typehints (recommended for Drupal) |
| 9 | Strictest - 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