Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    madsnorgaard

    drupal-expert

    madsnorgaard/drupal-expert
    Coding
    13
    2 installs

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Drupal 10/11 development expertise. Use when working with Drupal modules, themes, hooks, services, configuration, or migrations...

    SKILL.md

    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:

    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
    // 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
    // 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

    # 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

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

    Route with Permission

    my_module.page:
      path: '/my-page'
      defaults:
        _controller: '\Drupal\my_module\Controller\MyController::content'
        _title: 'My Page'
      requirements:
        _permission: 'access content'
    

    Plugin (Block Example)

    #[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!)

    # 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:

    // 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:

    $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

    # 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

    # 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

    # 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

    # 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

    # 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

    # 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:

    # 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:

    # 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:

    # 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:

    # After creating fields/content types via CLI
    drush config:export -y
    

    4. Document your scaffolding in README:

    ## 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

    # 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:

    # 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:

    # 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:

    # 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):

    # 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:

    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:

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

    Generate an event subscriber:

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

    Generate a Drush command:

    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:
      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:

    # Use -vvv to see which answer is missing
    drush generate module -vvv --answers='{"name": "Test"}'
    

    JSON parsing errors:

    # 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:

    # 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

    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

    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

    // 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
    {# 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):

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

    Use OOP hooks (D10.3+):

    // 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:

    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

    // 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

    # Run deprecation checks
    ./vendor/bin/drupal-check modules/custom/
    
    # Or with PHPStan
    ./vendor/bin/phpstan analyze modules/custom/ --level=5
    

    info.yml Compatibility

    # 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:

    # 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

    # 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)

    # 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

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

    Recommended: Full Pre-Commit Checklist

    # 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:

    #!/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

    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:

    # 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:

    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:

    **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

    ## 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

    ## 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:

    ✓ 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:

    # 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:

    # 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:

    # 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

    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

    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

    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:

    # 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
    • Services and Dependency Injection
    • Hooks vs Events
    • PHPUnit in Drupal
    • Drupal 11 Readiness
    • OOP Hooks
    • Drupal Recipes
    • Drush Code Generators
    • Drush Generate Command
    • Drush field:create
    • Scaffold Custom Content Entity with Drush
    • Drupal Code Generator (DCG)
    • Building a Drupal Module Using AI - Jacob Rockowitz
    • AI Generation Module
    • AI Module
    • CodeGenerator Agent Pattern
    Recommended Servers
    InfraNodus Knowledge Graphs & Text Analysis
    InfraNodus Knowledge Graphs & Text Analysis
    Microsoft Learn MCP
    Microsoft Learn MCP
    Repository
    madsnorgaard/agent-resources
    Files