Drupal coding standards for phpcs compliance. Covers cuddled brace style, docblock requirements, nullable parameter types, and general formatting. Load as a BASELINE for ALL Drupal code generation -- not a domain skill but a quality baseline loaded alongside any domain skill.
These patterns ensure generated code passes phpcs --standard=Drupal,DrupalPractice without errors.
Drupal uses "cuddled" control structures. Opening braces on the SAME line as the keyword. else, catch, finally cuddle with the closing brace.
WRONG: Brace on its own line or uncuddled else/catch:
if ($condition) { doSomething(); } else { doOther(); }
RIGHT: Cuddled braces --
} else {,} catch (...) {,} finally {all on one line:
if ($condition) {
doSomething();
}
else {
doOther();
}
try {
riskyOperation();
}
catch (SuspendQueueException $e) {
throw $e;
}
catch (\Exception $e) {
handleError($e);
}
Every class, method, function, and constant MUST have a docblock.
WRONG: Class or public method without docblock:
class MyController extends ControllerBase { public function build() { return ['#markup' => 'Hello']; } }
RIGHT: All classes and public methods get docblocks with @param/@return:
/** * Provides a page for displaying custom content. */ class MyController extends ControllerBase { /** * Builds the page content. * * @return array * A render array. */ public function build() { return ['#markup' => 'Hello']; } }
WRONG: Hook with @param/@return tags:
/** * Implements hook_cron(). * * @return void */ function my_module_cron() {
RIGHT: Hook implementations use ONLY the
Implementsline -- no @param or @return:/** * Implements hook_cron(). */ function my_module_cron() {
@file docblocks are ONLY for procedural files (.module, .install, .theme).
WRONG:
@filein a class file:<?php /** * @file * Contains \Drupal\my_module\Controller\MyController. */ namespace Drupal\my_module\Controller;
RIGHT: No
@filein class files. Only in procedural files:<?php /** * @file * Hook implementations for the My Module module. */
When a typed parameter defaults to NULL, the type MUST use ? nullable syntax.
WRONG: Implicit nullable (deprecated PHP 8.4, error in PHP 9):
public function init(ViewExecutable $view, DisplayPluginBase $display, array $options = NULL) {
RIGHT: Explicit
?typenullable:public function init(ViewExecutable $view, DisplayPluginBase $display, ?array $options = NULL) {
Critical when overriding parent methods with nullable parameters.
.php in src/ has exactly one class/interface/trait[] not array()?> tagif (, foreach (, while (The DrupalPractice phpcs standard flags service anti-patterns. The GlobalDrupal sniff fails any \Drupal:: static call inside a class file under src/.
WRONG:
\Drupal::service(),\Drupal::entityTypeManager(), or any\Drupal::call in controllers, forms, services, or list builders:class MyController extends ControllerBase { public function build() { $nodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple(); } }
RIGHT: Inject via
create()+ constructor.\Drupal::is ONLY valid in.moduleprocedural files:class MyController extends ControllerBase { public function __construct( protected EntityTypeManagerInterface $entityTypeManager, ) {} public static function create(ContainerInterface $container) { return new static($container->get('entity_type.manager')); } }
This applies to ALL classes: controllers, forms, list builders, event subscribers, services. Any class that can implement ContainerInjectionInterface MUST inject dependencies.