Skip to content

Drupal 10/11 Engineering Best Practices

Classification: PROCEDURE. Not authority; the governing standard below owns the rules.
Authority: drupal-standard
Applies to: All Drupal rigs, agents, recipes, and contrib modules across Bluefly.


1. Upstream Documentation First (Mandatory Preflight)

Before changing, configuring, extending, or debugging any contributed Drupal module: 1. Identify: Composer package name and machine name (composer.json). 2. Open Project Page: https://www.drupal.org/project/<machine_name> 3. Ecosystem Discovery: Check https://www.drupal.org/project/<machine_name>/ecosystem for companion modules and existing integration points. 4. Canonical Documentation: Follow the documentation link on the project page (project.pages.drupalcode.org/<project> or official guide). 5. Verify Version: Confirm composer.lock version matches documentation branch. 6. No Speculation: Never infer behavior from stale notes, memory, or random copies.


2. Coding & Architecture Invariants

2.1 Dependency Injection

  • Never call \Drupal::service(), \Drupal::currentUser(), or \Drupal::database() in classes that can receive dependencies (services, controllers, forms, plugins). The static wrapper is for procedural code only (.module, .install, hooks without a class) where injection is impossible (drupal.org: services and DI).
  • Use constructor injection via services.yml.
  • Implement ContainerInjectionInterface for Controllers, Forms, and Services.
  • Implement ContainerFactoryPluginInterface for all Plugins (Blocks, Field formatters, Tool API plugins).

2.2 OOP Hooks (Drupal 11+) vs Event Subscribers

  • OOP Hooks (#[Hook('form_alter')]): Use for altering core/contrib behavior where module weight matters.
  • Event Subscribers (KernelEvents::*, Symfony events): Use for decoupled, cross-cutting integrations, API responses, and external event listeners.

2.3 Configuration Schema (Mandatory)

  • Every custom config object or third-party setting MUST have an explicit schema in config/schema/<module>.schema.yml.
  • Schema strictness must pass drush config:inspect --show-errors.

2.4 Cache Metadata

  • Every render array, custom response, or computed data must carry full cache metadata:
    $build['#cache'] = [
      'tags' => ['node_list', 'ai_context_item_list'],
      'contexts' => ['user.permissions', 'url.query_args'],
      'max-age' => 3600,
    ];
    

2.5 Security & Parameterized Queries

  • Parameterized database queries only ($database->select() or $database->query() with placeholders).
  • Never concatenate raw strings or user input into queries.
  • Sanitize user strings with t('@variable') or Xss::filterAdmin().

3. CLI-First Scaffolding & Configuration Workflow

3.1 Drush Generation (Zero Manual Boilerplate)

Always use non-interactive Drush generators with --answers JSON:

# Non-interactive module generation
drush generate module --answers='{
  "name": "ContextControl Bridge",
  "machine_name": "contextcontrol_bridge",
  "package": "Custom",
  "dependencies": "ai_context:ai_context, eca:eca"
}'

3.2 Field & Content Type Creation

  • Use drush field:create instead of manual YAML crafting:
    drush field:create ai_context_item agent_memory \
      --field-name=field_kb_fingerprint \
      --field-type=string \
      --is-required=0
    
  • Always follow with drush config:export -y.

4. Production Release & Worktree Law

  1. Never edit code in web/core, web/modules/contrib, or web/themes/contrib.
  2. Fix producer packages in their owning git repository under BluCity/rigs/<name> or worktrees/.
  3. Create bead-named worktree, make changes, run tests, commit, push MR to release/v0.1.x.
  4. Verify CI green in GitLab (gitlab_components).
  5. Run composer update <package> in the consumer site repo to pull the released version.