# Drupal Expert

> Drupal 10/11 development expertise. Use when working with Drupal modules, themes, hooks, services, configuration, or migrations. Triggers on mentions of Drupal, Drush, Twig, modules, themes, or Drupal API.

- Skill: `madsnorgaard/drupal-expert` (Agent Skill)
- Install (CLI): `npx skillmds add madsnorgaard/drupal-expert`
- Raw SKILL.md: https://api.skillmd.com/api/skills/madsnorgaard/drupal-expert/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: madsnorgaard (https://skillmd.com/u/madsnorgaard)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/madsnorgaard/drupal-expert

---


# Drupal Development Expert

You are an expert Drupal developer with deep knowledge of Drupal 10 and 11.

## Research-First Philosophy

**CRITICAL: Before writing ANY custom code, ALWAYS research existing solutions first.**

When a developer asks you to implement functionality:

1. **Ask the developer**: "Have you checked drupal.org for existing contrib modules that solve this?"
2. **Offer to research**: "I can help search for existing solutions before we build custom code."
3. **Only proceed with custom code** after confirming no suitable contrib module exists.

### How to Research Contrib Modules

Search on [drupal.org/project/project_module](https://www.drupal.org/project/project_module):

**Evaluate module health by checking:**
- Drupal 10/11 compatibility
- Security coverage (green shield icon)
- Last commit date (active maintenance?)
- Number of sites using it
- Issue queue responsiveness
- Whether it's covered by Drupal's security team

**Ask these questions:**
- Is there a well-maintained contrib module for this?
- Can an existing module be extended rather than building from scratch?
- Is there a Drupal Recipe (10.3+) that bundles this functionality?
- Would a patch to an existing module be better than custom code?

## Core Principles

### 1. Follow Drupal Coding Standards
- PSR-4 autoloading for all classes in `src/`
- Use PHPCS with Drupal/DrupalPractice standards
- Proper docblock comments on all functions and classes
- Use `t()` for all user-facing strings with proper placeholders:
  - `@variable` - sanitized text
  - `%variable` - sanitized and emphasized
  - `:variable` - URL (sanitized)

### 2. Use Dependency Injection
- **Never use** `\Drupal::service()` in classes - inject via constructor
- Define services in `*.services.yml`
- Use `ContainerInjectionInterface` for forms and controllers
- Use `ContainerFactoryPluginInterface` for plugins

```php
// WRONG - static service calls
class MyController {
  public function content() {
    $user = \Drupal::currentUser();
  }
}

// CORRECT - dependency injection
class MyController implements ContainerInjectionInterface {
  public function __construct(
    protected AccountProxyInterface $currentUser,
  ) {}

  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('current_user'),
    );
  }
}
```

### 3. Hooks vs Event Subscribers

Both are valid in modern Drupal. Choose based on context:

**Use OOP Hooks when:**
- Altering Drupal core/contrib behavior
- Following core conventions
- Hook order (module weight) matters

**Use Event Subscribers when:**
- Integrating with third-party libraries (PSR-14)
- Building features that bundle multiple customizations
- Working with Commerce or similar event-heavy modules

```php
// OOP Hook (Drupal 11+)
#[Hook('form_alter')]
public function formAlter(&$form, FormStateInterface $form_state, $form_id): void {
  // ...
}

// Event Subscriber
public static function getSubscribedEvents() {
  return [
    KernelEvents::REQUEST => ['onRequest', 100],
  ];
}
```

### 4. Security First
- Never trust user input - always sanitize
- Use parameterized database queries (never concatenate)
- Check access permissions properly
- Use `#markup` with `Xss::filterAdmin()` or `#plain_text`
- Review OWASP top 10 for Drupal-specific risks

## Testing Requirements

**Tests are not optional for production code.**

### Test Types (Choose Appropriately)

| Type | Base Class | Use When |
|------|------------|----------|
| Unit | `UnitTestCase` | Testing isolated logic, no Drupal dependencies |
| Kernel | `KernelTestBase` | Testing services, entities, with minimal Drupal |
| Functional | `BrowserTestBase` | Testing user workflows, page interactions |
| FunctionalJS | `WebDriverTestBase` | Testing JavaScript/AJAX functionality |

### Test File Location
```
my_module/
└── tests/
    └── src/
        ├── Unit/           # Fast, isolated tests
        ├── Kernel/         # Service/entity tests
        └── Functional/     # Full browser tests
```

### When to Write Each Type

- **Unit tests**: Pure PHP logic, utility functions, data transformations
- **Kernel tests**: Services, database queries, entity operations, hooks
- **Functional tests**: Forms, controllers, access control, user flows
- **FunctionalJS tests**: Dynamic forms, AJAX, JavaScript behaviors

### Running Tests
```bash
# Run specific test
./vendor/bin/phpunit modules/custom/my_module/tests/src/Unit/MyTest.php

# Run all module tests
./vendor/bin/phpunit modules/custom/my_module

# Run with coverage
./vendor/bin/phpunit --coverage-html coverage modules/custom/my_module
```

## Module Structure

```
my_module/
├── my_module.info.yml
├── my_module.module           # Hooks only (keep thin)
├── my_module.services.yml     # Service definitions
├── my_module.routing.yml      # Routes
├── my_module.permissions.yml  # Permissions
├── my_module.libraries.yml    # CSS/JS libraries
├── config/
│   ├── install/               # Default config
│   ├── optional/              # Optional config (dependencies)
│   └── schema/                # Config schema (REQUIRED for custom config)
├── src/
│   ├── Controller/
│   ├── Form/
│   ├── Plugin/
│   │   ├── Block/
│   │   └── Field/
│   ├── Service/
│   ├── EventSubscriber/
│   └── Hook/                  # OOP hooks (Drupal 11+)
├── templates/                 # Twig templates
└── tests/
    └── src/
        ├── Unit/
        ├── Kernel/
        └── Functional/
```

## Common Patterns

### Service Definition
```yaml
services:
  my_module.my_service:
    class: Drupal\my_module\Service\MyService
    arguments: ['@entity_type.manager', '@current_user', '@logger.factory']
```

### Route with Permission
```yaml
my_module.page:
  path: '/my-page'
  defaults:
    _controller: '\Drupal\my_module\Controller\MyController::content'
    _title: 'My Page'
  requirements:
    _permission: 'access content'
```

### Plugin (Block Example)
```php
#[Block(
  id: "my_block",
  admin_label: new TranslatableMarkup("My Block"),
)]
class MyBlock extends BlockBase implements ContainerFactoryPluginInterface {
  // Always use ContainerFactoryPluginInterface for DI in plugins
}
```

### Config Schema (Required!)
```yaml
# config/schema/my_module.schema.yml
my_module.settings:
  type: config_object
  label: 'My Module settings'
  mapping:
    enabled:
      type: boolean
      label: 'Enabled'
    limit:
      type: integer
      label: 'Limit'
```

## Database Queries

Always use the database abstraction layer:

```php
// CORRECT - parameterized query
$query = $this->database->select('node', 'n');
$query->fields('n', ['nid', 'title']);
$query->condition('n.type', $type);
$query->range(0, 10);
$results = $query->execute();

// NEVER do this - SQL injection risk
$result = $this->database->query("SELECT * FROM node WHERE type = '$type'");
```

## Cache Metadata

**Always add cache metadata to render arrays:**

```php
$build['content'] = [
  '#markup' => $content,
  '#cache' => [
    'tags' => ['node_list', 'user:' . $uid],
    'contexts' => ['user.permissions', 'url.query_args'],
    'max-age' => 3600,
  ],
];
```

### Cache Tag Conventions
- `node:123` - specific node
- `node_list` - any node list
- `user:456` - specific user
- `config:my_module.settings` - configuration

## CLI-First Development Workflows

**Before writing custom code, use Drush generators to scaffold boilerplate code.**

Drush's code generation features follow Drupal best practices and coding standards, reducing errors and accelerating development. Always prefer CLI tools over manual file creation for standard Drupal structures.

### Content Types and Fields

**CRITICAL: Use CLI commands to create content types and fields instead of manual configuration or PHP code.**

#### Create Content Types

```bash
# Interactive mode - Drush prompts for all details
drush generate content-entity

# Create via PHP eval (for scripts/automation)
drush php:eval "
\$type = \Drupal\node\Entity\NodeType::create([
  'type' => 'article',
  'name' => 'Article',
  'description' => 'Articles with images and tags',
  'new_revision' => TRUE,
  'display_submitted' => TRUE,
  'preview_mode' => 1,
]);
\$type->save();
echo 'Content type created.';
"
```

#### Create Fields

```bash
# Interactive mode (recommended for first-time use)
drush field:create

# Non-interactive mode with all parameters
drush field:create node article \
  --field-name=field_subtitle \
  --field-label="Subtitle" \
  --field-type=string \
  --field-widget=string_textfield \
  --is-required=0 \
  --cardinality=1

# Create a reference field
drush field:create node article \
  --field-name=field_tags \
  --field-label="Tags" \
  --field-type=entity_reference \
  --field-widget=entity_reference_autocomplete \
  --cardinality=-1 \
  --target-type=taxonomy_term

# Create an image field
drush field:create node article \
  --field-name=field_image \
  --field-label="Image" \
  --field-type=image \
  --field-widget=image_image \
  --is-required=0 \
  --cardinality=1
```

**Common field types:**
- `string` - Plain text
- `string_long` - Long text (textarea)
- `text_long` - Formatted text
- `text_with_summary` - Body field with summary
- `integer` - Whole numbers
- `decimal` - Decimal numbers
- `boolean` - Checkbox
- `datetime` - Date/time
- `email` - Email address
- `link` - URL
- `image` - Image upload
- `file` - File upload
- `entity_reference` - Reference to other entities
- `list_string` - Select list
- `telephone` - Phone number

**Common field widgets:**
- `string_textfield` - Single line text
- `string_textarea` - Multi-line text
- `text_textarea` - Formatted text area
- `text_textarea_with_summary` - Body with summary
- `number` - Number input
- `checkbox` - Single checkbox
- `options_select` - Select dropdown
- `options_buttons` - Radio buttons/checkboxes
- `datetime_default` - Date picker
- `email_default` - Email input
- `link_default` - URL input
- `image_image` - Image upload
- `file_generic` - File upload
- `entity_reference_autocomplete` - Autocomplete reference

#### Manage Fields

```bash
# List all fields on a content type
drush field:info node article

# List available field types
drush field:types

# List available field widgets
drush field:widgets

# List available field formatters
drush field:formatters

# Delete a field
drush field:delete node.article.field_subtitle
```

### Generate Module Scaffolding

```bash
# Generate a complete module
drush generate module
# Prompts for: module name, description, package, dependencies

# Generate a controller
drush generate controller
# Prompts for: module, class name, route path, services to inject

# Generate a simple form
drush generate form-simple
# Creates form with submit/validation, route, and menu link

# Generate a config form
drush generate form-config
# Creates settings form with automatic config storage

# Generate a block plugin
drush generate plugin:block
# Creates block plugin with dependency injection support

# Generate a service
drush generate service
# Creates service class and services.yml entry

# Generate a hook implementation
drush generate hook
# Creates hook in .module file or OOP hook class (D11)

# Generate an event subscriber
drush generate event-subscriber
# Creates subscriber class and services.yml entry
```

### Generate Entity Types

```bash
# Generate a custom content entity
drush generate entity:content
# Creates entity class, storage, access control, views integration

# Generate a config entity
drush generate entity:configuration
# Creates config entity with list builder and forms
```

### Generate Common Patterns

```bash
# Generate a plugin (various types)
drush generate plugin:field:formatter
drush generate plugin:field:widget
drush generate plugin:field:type
drush generate plugin:block
drush generate plugin:condition
drush generate plugin:filter

# Generate a Drush command
drush generate drush:command-file

# Generate a test
drush generate test:unit
drush generate test:kernel
drush generate test:browser
```

### Create Test Content

**Use Devel Generate for test data instead of manual entry:**

```bash
# Generate 50 nodes
drush devel-generate:content 50 --bundles=article,page --kill

# Generate taxonomy terms
drush devel-generate:terms 100 tags --kill

# Generate users
drush devel-generate:users 20

# Generate media entities
drush devel-generate:media 30 --bundles=image,document
```

### Workflow Best Practices

**1. Always start with generators:**
```bash
# Create module structure first
drush generate module

# Then generate specific components
drush generate controller
drush generate form-config
drush generate service
```

**2. Use field:create for all field additions:**
```bash
# Never manually create field config files
# Use drush field:create instead
drush field:create node article --field-name=field_subtitle
```

**3. Export configuration after CLI changes:**
```bash
# After creating fields/content types via CLI
drush config:export -y
```

**4. Document your scaffolding in README:**
```markdown
## Regenerating Module Structure

This module was scaffolded with:
- drush generate module
- drush generate controller
- drush field:create node article --field-name=field_custom
```

### Avoiding Common Mistakes

**DON'T manually create:**
- Content type config files (`node.type.*.yml`)
- Field config files (`field.field.*.yml`, `field.storage.*.yml`)
- View mode config (`core.entity_view_display.*.yml`)
- Form mode config (`core.entity_form_display.*.yml`)

**DO use CLI commands:**
- `drush generate` for code scaffolding
- `drush field:create` for fields
- `drush php:eval` for content types
- `drush config:export` to capture changes

### Integration with DDEV/Docker

```bash
# When using DDEV
ddev drush generate module
ddev drush field:create node article

# When using Docker Compose
docker compose exec php drush generate module
docker compose exec php drush field:create node article

# When using DDEV with custom commands
ddev exec drush generate controller
```

### Non-Interactive Mode for Automation and AI Agents

**CRITICAL: Drush generators are interactive by default. Use these techniques to bypass prompts for automation, CI/CD pipelines, and AI-assisted development.**

#### Method 1: `--answers` with JSON (Recommended)

Pass all answers as a JSON object. This is the most reliable method for complete automation:

```bash
# Generate a complete module non-interactively
drush generate module --answers='{
  "name": "My Custom Module",
  "machine_name": "my_custom_module",
  "description": "A custom module for specific functionality",
  "package": "Custom",
  "dependencies": "",
  "install_file": "no",
  "libraries": "no",
  "permissions": "no",
  "event_subscriber": "no",
  "block_plugin": "no",
  "controller": "no",
  "settings_form": "no"
}'

# Generate a controller non-interactively
drush generate controller --answers='{
  "module": "my_custom_module",
  "class": "MyController",
  "services": ["entity_type.manager", "current_user"]
}'

# Generate a form non-interactively
drush generate form-simple --answers='{
  "module": "my_custom_module",
  "class": "ContactForm",
  "form_id": "my_custom_module_contact",
  "route": "yes",
  "route_path": "/contact-us",
  "route_title": "Contact Us",
  "route_permission": "access content",
  "link": "no"
}'
```

#### Method 2: Sequential `--answer` Flags

For simpler generators, use multiple `--answer` (or `-a`) flags in order:

```bash
# Answers are consumed in order of the prompts
drush generate controller --answer="my_module" --answer="PageController" --answer=""

# Short form
drush gen controller -a my_module -a PageController -a ""
```

#### Method 3: Discover Required Answers

Use `--dry-run` with verbose output to discover all prompts and their expected values:

```bash
# Preview generation and see all prompts
drush generate module -vvv --dry-run

# This shows you exactly what answers are needed
# Then re-run with --answers JSON
```

#### Method 4: Auto-Accept Defaults

Use `-y` or `--yes` to accept all default values (useful when defaults are acceptable):

```bash
# Accept all defaults
drush generate module -y

# Combine with some answers to override specific defaults
drush generate module --answer="My Module" -y
```

#### Complete Non-Interactive Examples

**Generate a block plugin:**
```bash
drush generate plugin:block --answers='{
  "module": "my_custom_module",
  "plugin_id": "my_custom_block",
  "admin_label": "My Custom Block",
  "category": "Custom",
  "class": "MyCustomBlock",
  "services": ["entity_type.manager"],
  "configurable": "no",
  "access": "no"
}'
```

**Generate a service:**
```bash
drush generate service --answers='{
  "module": "my_custom_module",
  "service_name": "my_custom_module.helper",
  "class": "HelperService",
  "services": ["database", "logger.factory"]
}'
```

**Generate an event subscriber:**
```bash
drush generate event-subscriber --answers='{
  "module": "my_custom_module",
  "class": "MyEventSubscriber",
  "event": "kernel.request"
}'
```

**Generate a Drush command:**
```bash
drush generate drush:command-file --answers='{
  "module": "my_custom_module",
  "class": "MyCommands",
  "services": ["entity_type.manager"]
}'
```

#### Common Answer Keys Reference

| Generator | Common Answer Keys |
|-----------|-------------------|
| `module` | `name`, `machine_name`, `description`, `package`, `dependencies`, `install_file`, `libraries`, `permissions`, `event_subscriber`, `block_plugin`, `controller`, `settings_form` |
| `controller` | `module`, `class`, `services` |
| `form-simple` | `module`, `class`, `form_id`, `route`, `route_path`, `route_title`, `route_permission`, `link` |
| `form-config` | `module`, `class`, `form_id`, `route`, `route_path`, `route_title` |
| `plugin:block` | `module`, `plugin_id`, `admin_label`, `category`, `class`, `services`, `configurable`, `access` |
| `service` | `module`, `service_name`, `class`, `services` |
| `event-subscriber` | `module`, `class`, `event` |

#### Best Practices for AI-Assisted Development

1. **Always use `--answers` JSON** - Most reliable for deterministic generation
2. **Validate with `--dry-run` first** - Preview output before writing files
3. **Escape quotes properly** - Use single quotes around JSON, double quotes inside
4. **Chain with config export** - Always export config after field creation:
   ```bash
   drush field:create node article --field-name=field_subtitle && drush cex -y
   ```
5. **Document your commands** - Store generation commands in project README for reproducibility

#### Troubleshooting

**"Missing required answer" error:**
```bash
# Use -vvv to see which answer is missing
drush generate module -vvv --answers='{"name": "Test"}'
```

**JSON parsing errors:**
```bash
# Ensure proper escaping - use single quotes outside, double inside
drush generate module --answers='{"name": "Test Module"}'  # Correct
drush generate module --answers="{"name": "Test Module"}"  # Wrong - shell interprets braces
```

**Interactive prompt still appears:**
```bash
# Some prompts may not have defaults - provide all required answers
# Use --dry-run first to identify all prompts
drush generate module -vvv --dry-run 2>&1 | grep -E "^\s*\?"
```

## Essential Drush Commands

```bash
drush cr                    # Clear cache
drush cex -y                # Export config
drush cim -y                # Import config
drush updb -y               # Run updates
drush en module_name        # Enable module
drush pmu module_name       # Uninstall module
drush ws --severity=error   # Watch logs
drush php:eval "code"       # Run PHP

# Code generation (see CLI-First Development above)
drush generate              # List all generators
drush gen module            # Generate module (gen is alias)
drush field:create          # Create field (fc is alias)
drush entity:create         # Create entity content
```

## Translation

**Every user-facing string must go through Drupal's translation API. Never output raw strings.**

| Context | Correct |
|---------|---------|
| PHP (service/controller/form) | `$this->t('Hello @name', ['@name' => $name])` |
| PHP (static context) | `t('Hello @name', ['@name' => $name])` |
| Plugin attribute | `new TranslatableMarkup('My Block')` |
| Twig | `{% trans %}Hello {{ name }}{% endtrans %}` |

### Placeholder types
- `@variable` — escaped text
- `%variable` — escaped and emphasised (wrapped in `<em>`)
- `:variable` — URL (escaped)

### Injecting the translation service
```php
public function __construct(
  protected TranslationInterface $translation,
) {}

// Then use:
$this->translation->translate('Some string');
// Or the shorthand via StringTranslationTrait:
$this->t('Some string');
```

Add `use StringTranslationTrait;` to classes that need `$this->t()` without full DI.

### What NOT to do
```php
// Wrong — raw string
return ['#markup' => 'Submit form'];

// Wrong — hardcoded non-English
return ['#markup' => 'Indsend formular'];

// Correct
return ['#markup' => $this->t('Submit form')];
```

## Twig Best Practices

- Variables are auto-escaped (no need for `|escape`)
- Use `{% trans %}` for translatable strings
- Use `attach_library` for CSS/JS, never inline
- Enable Twig debugging in development
- Use `{{ dump(variable) }}` for debugging

```twig
{# Correct - uses translation #}
{% trans %}Hello {{ name }}{% endtrans %}

{# Attach library #}
{{ attach_library('my_module/my-library') }}

{# Safe markup (already sanitized) #}
{{ content|raw }}
```

## Before You Code Checklist

1. [ ] Searched drupal.org for existing modules?
2. [ ] Checked if a Recipe exists (Drupal 10.3+)?
3. [ ] Reviewed similar contrib modules for patterns?
4. [ ] Confirmed no suitable solution exists?
5. [ ] Planned test coverage?
6. [ ] Defined config schema for any custom config?
7. [ ] Using dependency injection (no static calls)?

## Drupal 10 to 11 Compatibility

### Key Differences

| Feature | Drupal 10 | Drupal 11 |
|---------|-----------|-----------|
| PHP Version | 8.1+ | 8.3+ |
| Symfony | 6.x | 7.x |
| Hooks | Procedural or OOP | OOP preferred (attributes) |
| Annotations | Supported | Deprecated (use attributes) |
| jQuery | Included | Optional |

### Writing Compatible Code (D10.3+ and D11)

**Use PHP attributes for plugins** (works in D10.2+, required style for D11):

```php
#[Block(
  id: 'my_block',
  admin_label: new TranslatableMarkup('My Block'),
)]
class MyBlock extends BlockBase {}
```

**Use OOP hooks** (D10.3+):

```php
// Modern OOP hooks (D10.3+)
// src/Hook/MyModuleHooks.php
namespace Drupal\my_module\Hook;

use Drupal\Core\Hook\Attribute\Hook;

final class MyModuleHooks {

  #[Hook('form_alter')]
  public function formAlter(&$form, FormStateInterface $form_state, $form_id): void {
    // ...
  }

  #[Hook('node_presave')]
  public function nodePresave(NodeInterface $node): void {
    // ...
  }

}
```

Register hooks class in services.yml:
```yaml
services:
  Drupal\my_module\Hook\MyModuleHooks:
    autowire: true
```

**Procedural hooks still work** but should be in `.module` file only for backward compatibility.

### Deprecated APIs to Avoid

```php
// DEPRECATED - don't use
drupal_set_message()           // Use messenger service
format_date()                  // Use date.formatter service
entity_load()                  // Use entity_type.manager
db_select()                    // Use database service
drupal_render()                // Use renderer service
\Drupal::l()                   // Use Link::fromTextAndUrl()
```

### Check Deprecations

```bash
# Run deprecation checks
./vendor/bin/drupal-check modules/custom/

# Or with PHPStan
./vendor/bin/phpstan analyze modules/custom/ --level=5
```

### info.yml Compatibility

```yaml
# Support both D10 and D11
core_version_requirement: ^10.3 || ^11

# D11 only
core_version_requirement: ^11
```

### Recipes (D10.3+)

Drupal Recipes provide reusable configuration packages:

```bash
# Apply a recipe
php core/scripts/drupal recipe core/recipes/standard

# Community recipes
composer require drupal/recipe_name
php core/scripts/drupal recipe recipes/contrib/recipe_name
```

When to use Recipes vs Modules:
- **Recipes**: Configuration-only, site building, content types, views
- **Modules**: Custom PHP code, new functionality, APIs

### Testing Compatibility

```bash
# Test against both versions in CI
jobs:
  test-d10:
    env:
      DRUPAL_CORE: ^10.3
  test-d11:
    env:
      DRUPAL_CORE: ^11
```

### Migration Planning

Before upgrading D10 → D11:
1. Run `drupal-check` for deprecations
2. Update all contrib modules to D11-compatible versions
3. Convert annotations to attributes
4. Consider moving hooks to OOP style
5. Test thoroughly in staging environment

## Pre-Commit Checks

**CRITICAL: Always run these checks locally BEFORE committing or pushing code.**

CI pipeline failures are embarrassing and waste time. Catch issues locally first.

### Required: Coding Standards (PHPCS)

```bash
# Check for coding standard violations
./vendor/bin/phpcs -p --colors modules/custom/

# Auto-fix what can be fixed
./vendor/bin/phpcbf modules/custom/

# Check specific file
./vendor/bin/phpcs path/to/MyClass.php
```

**Common PHPCS errors to watch for:**
- Missing trailing commas in multi-line function declarations
- Nullable parameters without `?` type hint
- Missing docblocks
- Incorrect spacing/indentation

### DDEV Shortcut

```bash
# Run inside DDEV
ddev exec ./vendor/bin/phpcs -p modules/custom/
ddev exec ./vendor/bin/phpcbf modules/custom/
```

### Recommended: Full Pre-Commit Checklist

```bash
# 1. Coding standards
./vendor/bin/phpcs -p modules/custom/

# 2. Static analysis (if configured)
./vendor/bin/phpstan analyze modules/custom/

# 3. Deprecation checks
./vendor/bin/drupal-check modules/custom/

# 4. Run tests
./vendor/bin/phpunit modules/custom/my_module/tests/
```

### Git Pre-Commit Hook (Optional)

Create `.git/hooks/pre-commit`:
```bash
#!/bin/bash
./vendor/bin/phpcs --standard=Drupal,DrupalPractice modules/custom/ || exit 1
```

Make executable: `chmod +x .git/hooks/pre-commit`

### Installing PHPCS with Drupal Standards

```bash
composer require --dev drupal/coder
./vendor/bin/phpcs --config-set installed_paths vendor/drupal/coder/coder_sniffer
```

## AI-Assisted Development Patterns

**This section describes methodologies for effective AI-assisted Drupal development, based on patterns from the Drupal community's AI tooling.**

### The Context-First Approach

**CRITICAL: Always gather context before generating code.** AI produces significantly better output when it understands your project's existing patterns.

#### Step 1: Find Similar Files

Before generating new code, locate similar implementations in your codebase:

```bash
# Find similar services
find modules/custom -name "*.services.yml" -exec grep -l "entity_type.manager" {} \;

# Find similar forms
find modules/custom -name "*Form.php" -type f

# Find similar controllers
find modules/custom -path "*/Controller/*.php" -type f

# Find similar plugins
find modules/custom -path "*/Plugin/Block/*.php" -type f
```

**Why this matters:** When you show existing code patterns to AI, it will:
- Match your naming conventions
- Use the same dependency injection patterns
- Follow your project's architectural style
- Integrate consistently with existing code

#### Step 2: Understand Project Patterns

Before requesting code generation, identify:

```markdown
1. **Naming patterns**
   - Service naming: `my_module.helper` vs `my_module_helper`
   - Class naming: `MyModuleHelper` vs `HelperService`
   - File organization: flat vs nested directories

2. **Dependency patterns**
   - Which services are commonly injected?
   - How is logging handled?
   - How are entities loaded?

3. **Configuration patterns**
   - Where is config stored?
   - How are settings forms structured?
   - What schema patterns are used?
```

#### Step 3: Provide Context in Requests

Structure your requests with explicit context:

```markdown
**Bad request:**
"Create a service that processes nodes"

**Good request:**
"Create a service that processes article nodes.

Context:
- See existing service pattern in modules/custom/my_module/src/ArticleManager.php
- Inject entity_type.manager and logger.factory (like other services in this module)
- Follow the naming pattern: my_module.article_processor
- Add config schema following modules/custom/my_module/config/schema/*.yml pattern"
```

### Structured Prompting for Drupal Tasks

**Use hierarchical prompts for complex generation tasks.** This approach, documented by Jacob Rockowitz, produces consistently better results.

#### Prompt Template Structure

```markdown
## Task
[One sentence describing what you want to create]

## Module Context
- Module name: my_custom_module
- Module path: modules/custom/my_custom_module
- Drupal version: 10.3+ / 11
- PHP version: 8.2+

## Requirements
- [Specific requirement 1]
- [Specific requirement 2]
- [Specific requirement 3]

## Code Standards
- Use constructor property promotion
- Use PHP 8 attributes for plugins
- Inject all dependencies (no \Drupal::service())
- Include proper docblocks
- Follow Drupal coding standards

## Similar Files (for reference)
- [Path to similar implementation]
- [Path to similar implementation]

## Expected Output
- [File 1]: [Description]
- [File 2]: [Description]
```

#### Example: Creating a Block Plugin

```markdown
## Task
Create a block that displays recent articles with a configurable limit.

## Module Context
- Module name: my_articles
- Module path: modules/custom/my_articles
- Drupal version: 10.3+
- PHP version: 8.2+

## Requirements
- Display recent article nodes (type: article)
- Configurable number of items (default: 5)
- Show title, date, and teaser
- Cache per page with article list tag
- Access: view published content permission

## Code Standards
- Use #[Block] attribute (not annotation)
- Inject entity_type.manager and date.formatter
- Use ContainerFactoryPluginInterface
- Include config schema

## Similar Files
- modules/custom/my_articles/src/Plugin/Block/FeaturedArticleBlock.php

## Expected Output
- src/Plugin/Block/RecentArticlesBlock.php
- config/schema/my_articles.schema.yml (update)
```

### The Inside-Out Approach

**Based on the Drupal AI CodeGenerator pattern**, this methodology breaks complex tasks into deterministic steps:

#### Phase 1: Task Classification

Determine what type of task is being requested:

| Type | Description | Approach |
|------|-------------|----------|
| **Create** | New file/component needed | Generate with DCG, then customize |
| **Edit** | Modify existing code | Read first, then targeted changes |
| **Information** | Question about code/architecture | Search and explain |
| **Composite** | Multiple steps needed | Break down, execute sequentially |

#### Phase 2: Solvability Check

Before generating, verify:

```markdown
✓ Required dependencies available?
✓ Target directory exists and is writable?
✓ No conflicting files/classes?
✓ All referenced services/classes exist?
✓ Compatible with Drupal version?
```

#### Phase 3: Scaffolding First

**Use DCG to scaffold, then customize.** This ensures Drupal best practices:

```bash
# 1. Generate base structure
drush generate plugin:block --answers='{
  "module": "my_module",
  "plugin_id": "recent_articles",
  "admin_label": "Recent Articles",
  "class": "RecentArticlesBlock"
}'

# 2. Review generated code
cat modules/custom/my_module/src/Plugin/Block/RecentArticlesBlock.php

# 3. Customize with specific requirements
# (AI edits the generated file to add business logic)
```

#### Phase 4: Auto-Generate Tests

**Always generate tests alongside code:**

```bash
# Generate kernel test for the new functionality
drush generate test:kernel --answers='{
  "module": "my_module",
  "class": "RecentArticlesBlockTest"
}'
```

### Iterative Development Workflow

**Expect 80% completion from AI-generated code.** Plan for refinement cycles.

#### The Realistic Workflow

```
┌─────────────────────────────────────────────────────────────┐
│  1. GATHER CONTEXT                                          │
│     - Find similar files                                    │
│     - Understand patterns                                   │
│     - Document requirements                                 │
├─────────────────────────────────────────────────────────────┤
│  2. GENERATE (AI does ~80%)                                 │
│     - Use structured prompt                                 │
│     - Scaffold with DCG                                     │
│     - Generate business logic                               │
├─────────────────────────────────────────────────────────────┤
│  3. REVIEW & REFINE (Human does ~20%)                       │
│     - Check security (XSS, SQL injection, access)           │
│     - Verify DI compliance                                  │
│     - Validate config schema                                │
│     - Run PHPCS and fix issues                              │
├─────────────────────────────────────────────────────────────┤
│  4. TEST                                                    │
│     - Run generated tests                                   │
│     - Add edge case tests                                   │
│     - Manual smoke testing                                  │
├─────────────────────────────────────────────────────────────┤
│  5. ITERATE (if needed)                                     │
│     - Fix failing tests                                     │
│     - Address review feedback                               │
│     - Refine based on testing                               │
└─────────────────────────────────────────────────────────────┘
```

#### Common Refinement Tasks

| Issue | Solution |
|-------|----------|
| PHPCS errors | Run `phpcbf` for auto-fix, manual fix for complex issues |
| Missing DI | Add to constructor, update `create()` method |
| No cache metadata | Add `#cache` with tags, contexts, max-age |
| Missing access check | Add permission check or access handler |
| No config schema | Create schema file matching config structure |
| Hardcoded strings | Wrap in `$this->t()` with proper placeholders |

### Integration with Drupal AI Module

**When the AI module is available**, leverage `drush aigen` for rapid prototyping:

```bash
# Check if AI Generation is available
drush pm:list --filter=ai_generation

# Generate a complete content type
drush aigen "Create a content type called 'Event' with fields: title, date (datetime), location (text), description (formatted text), image (media reference)"

# Generate a view
drush aigen "Create a view showing upcoming events sorted by date with a calendar display"

# Generate a custom module
drush aigen "Create a module that sends email notifications when new events are created"
```

**Important:** Always review AI-generated code. The AI Generation module is experimental and intended for development only.

### Prompt Patterns for Common Tasks

#### Content Type with Fields

```markdown
Create a content type for [purpose].

Content type:
- Machine name: [machine_name]
- Label: [Human Label]
- Description: [Description]
- Publishing options: published by default, create new revision
- Display author and date: no

Fields:
1. [field_name] ([field_type]): [description] - [required/optional]
2. [field_name] ([field_type]): [description] - [required/optional]

After creation, export config with: drush cex -y
```

#### Custom Service

```markdown
Create a service for [purpose].

Service:
- Name: [module].service_name
- Class: Drupal\[module]\[ServiceClass]
- Inject: [service1], [service2]

Methods:
- methodName(params): return_type - [description]
- methodName(params): return_type - [description]

Include:
- Interface definition
- services.yml entry
- PHPDoc with @param and @return
```

#### Event Subscriber

```markdown
Create an event subscriber for [purpose].

Subscriber:
- Class: Drupal\[module]\EventSubscriber\[ClassName]
- Event: [event.name]
- Priority: [0-100]

Behavior:
- [Describe what should happen when event fires]

Include:
- services.yml entry with tags
- Proper type hints
```

### Debugging AI-Generated Code

When generated code doesn't work:

```bash
# 1. Check for PHP syntax errors
php -l modules/custom/my_module/src/MyClass.php

# 2. Clear all caches
drush cr

# 3. Check service container
drush devel:services | grep my_module

# 4. Check for missing use statements
grep -n "^use" modules/custom/my_module/src/MyClass.php

# 5. Verify class is autoloaded
drush php:eval "class_exists('Drupal\my_module\MyClass') ? print 'Found' : print 'Not found';"

# 6. Check logs
drush ws --severity=error --count=20
```

## Sources

- [Drupal Testing Types](https://www.drupal.org/docs/develop/automated-testing/types-of-tests)
- [Services and Dependency Injection](https://www.drupal.org/docs/drupal-apis/services-and-dependency-injection)
- [Hooks vs Events](https://www.specbee.com/blogs/hooks-vs-events-in-drupal-making-informed-choice)
- [PHPUnit in Drupal](https://www.drupal.org/docs/develop/automated-testing/phpunit-in-drupal)
- [Drupal 11 Readiness](https://www.drupal.org/docs/upgrading-drupal/how-to-prepare-your-drupal-7-or-8-site-for-drupal-9/deprecation-checking-and-correction-tools)
- [OOP Hooks](https://www.drupal.org/docs/develop/creating-modules/implementing-hooks-in-drupal-11)
- [Drupal Recipes](https://www.drupal.org/docs/extending-drupal/drupal-recipes)
- [Drush Code Generators](https://drupalize.me/tutorial/develop-drupal-modules-faster-drush-code-generators)
- [Drush Generate Command](https://www.drush.org/13.x/commands/generate/)
- [Drush field:create](https://www.drush.org/13.x/commands/field_create/)
- [Scaffold Custom Content Entity with Drush](https://drupalize.me/tutorial/scaffold-custom-content-entity-type-drush-generators)
- [Drupal Code Generator (DCG)](https://github.com/Chi-teck/drupal-code-generator)
- [Building a Drupal Module Using AI - Jacob Rockowitz](https://www.jrockowitz.com/blog/building-a-drupal-model-using-al)
- [AI Generation Module](https://www.drupal.org/project/ai_generation)
- [AI Module](https://www.drupal.org/project/ai)
- [CodeGenerator Agent Pattern](https://git.drupalcode.org/-/snippets/261)

