Always run PHPCS before committing. Never skip static analysis. Use declare(strict_types=1) in every PHP file. Prefer final classes unless explicitly designed for extension. Avoid deep nesting — use guard clauses and early returns.
Validation Commands
# Check coding standards.
./vendor/bin/phpcs --standard=Drupal,DrupalPractice web/modules/custom/my_module/
# Auto-fix standards issues.
./vendor/bin/phpcbf --standard=Drupal web/modules/custom/my_module/
# Static analysis.
./vendor/bin/phpstan analyse web/modules/custom/my_module/
# Deprecation check.
drupal-check web/modules/custom/my_module/
Security audit.
composer audit
## Required Code Patterns
### File Headers
```php
<?php
declare(strict_types=1);
namespace Drupal\my_module;
Class Declaration
/**
* Manages content operations.
*/
final class ContentManager implements ContentManagerInterface {
Constructor
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly LoggerInterface $logger,
) {}
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Local variables | $snake_case |
$user_name |
| Class properties | $lowerCamelCase |
$this->entityManager |
| Classes | PascalCase |
ContentManager |
| Interfaces | PascalCase + Interface |
ContentManagerInterface |
| Constants | UPPER_SNAKE_CASE |
MAX_RETRY_COUNT |
| Services | module.service_name |
my_module.content_manager |
| Hooks | module_hook_name |
my_module_form_alter |
Anti-Patterns to Report
| Anti-Pattern | Fix |
|---|---|
foreach with nested if/break/continue |
array_filter/array_map/array_reduce |
| Deep nesting (3+ levels) | Guard clauses, early returns |
| Non-final classes without reason | Declare final by default |
Getters/setters where public readonly works |
Use public readonly |
\Drupal:: calls in classes |
Constructor dependency injection |
PHP json_encode/decode |
\GuzzleHttp\Utils::jsonDecode/jsonEncode |
| "Service" namespace | Use logical groupings |
Catching \Exception |
Catch narrowest exception type |
PHPDoc Standards
/**
* Loads articles by category.
*
* @param int $category_id
* The taxonomy term ID.
* @param int $limit
* Maximum number of results.
*
* @return \Drupal\node\NodeInterface[]
* Array of article nodes.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
public function loadByCategory(int $category_id, int $limit = 10): array {
Module Structure
modules/custom/my_module/
my_module.info.yml
my_module.module
my_module.services.yml
my_module.routing.yml
my_module.permissions.yml
config/
install/
schema/
src/
Controller/
Form/
Plugin/
Block/
EventSubscriber/
tests/
src/
Unit/
Kernel/
Functional/