drupal-expert
π―Skillfrom madsnorgaard/agent-resources
Provides expert guidance on Drupal development, module creation, theming, performance optimization, and best practices for building robust web applications
Installation
npx skills add https://github.com/madsnorgaard/agent-resources --skill drupal-expertSkill Details
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.
Overview
# 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:
- Ask the developer: "Have you checked drupal.org for existing contrib modules that solve this?"
- Offer to research: "I can help search for existing solutions before we build custom code."
- 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
ContainerInjectionInterfacefor forms and controllers - Use
ContainerFactoryPluginInterfacefor 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
#markupwithXss::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 nodenode_list- any node listuser:456- specific userconfig: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 textstring_long- Long text (textarea)text_long- Formatted texttext_with_summary- Body field with summaryinteger- Whole numbersdecimal- Decimal numbersboolean- Checkboxdatetime- Date/timeemail- Email addresslink- URLimage- Image uploadfile- File uploadentity_reference- Reference to other entitieslist_string- Select listtelephone- Phone number
Common field widgets:
string_textfield- Single line textstring_textarea- Multi-line texttext_textarea- Formatted text areatext_textarea_with_summary- Body with summarynumber- Number inputcheckbox- Single checkboxoptions_select- Select dropdownoptions_buttons- Radio buttons/checkboxesdatetime_default- Date pickeremail_default- Email inputlink_default- URL inputimage_image- Image uploadfile_generic- File uploadentity_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 generatefor code scaffoldingdrush field:createfor fieldsdrush php:evalfor content typesdrush config:exportto 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
- Always use
--answersJSON - Most reliable for deterministic generation - Validate with
--dry-runfirst - Preview output before writing files - Escape quotes properly - Use single quotes around JSON, double quotes inside
- Chain with config export - Always export config after field creation:
```bash
drush field:create node article --field-name=field_subtitle && drush cex -y
```
- 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
```
Twig Best Practices
- Variables are auto-escaped (no need for
|escape) - Use
{% trans %}for translatable strings - Use
attach_libraryfor 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
- [ ] Searched drupal.org for existing modules?
- [ ] Checked if a Recipe exists (Drupal 10.3+)?
- [ ] Reviewed similar contrib modules for patterns?
- [ ] Confirmed no suitable solution exists?
- [ ] Planned test coverage?
- [ ] Defined config schema for any custom config?
- [ ] 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
// Modern style (D10.2+, required for D11)
#[Block(
id: 'my_block',
admin_label: new TranslatableMarkup('My Block'),
)]
class MyBlock extends BlockBase {}
// Legacy style (still works but discouraged)
/**
* @Block(
* id = "my_block",
* admin_label = @Translation("My Block"),
* )
*/
```
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:
- Run
drupal-checkfor deprecations - Update all contrib modules to D11-compatible versions
- Convert annotations to attributes
- Consider moving hooks to OOP style
- 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
- Naming patterns
- Service naming: my_module.helper vs my_module_helper
- Class naming: MyModuleHelper vs HelperService
- File organization: flat vs nested directories
- Dependency patterns
- Which services are commonly injected?
- How is logging handled?
- How are entities loaded?
- 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:
- [field_name] ([field_type]): [description] - [required/optional]
- [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)
More from this repository4
Facilitates Drupal migrations by automating data transfer from Drupal 7 to Drupal 10, supporting CSV, JSON, and custom migration workflows.
Streamlines Docker Compose local development workflows with essential commands for managing containers, running services, and debugging Docker environments.
Provides expert guidance and commands for managing DDEV local development environments, Docker containers, and PHP project configurations.
Proactively prevents security vulnerabilities in Drupal by auto-detecting and blocking XSS, SQL injection, and access control risks during code development.