diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..d49bc5a --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,72 @@ +name: Validate AGENTS.md + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint: + name: Lint & Validate + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Lint Markdown files + run: npm run lint + + - name: Check formatting + run: npx prettier --check '**/*.md' + + - name: Check markdown code fences + run: | + errors=0 + while IFS= read -r -d '' file; do + opens=$(grep -c '^\s*```' "$file" || true) + if [ $((opens % 2)) -ne 0 ]; then + echo "ERROR: Unclosed code fence in $file (found $opens fence markers)" + errors=$((errors + 1)) + fi + done < <(find . -name "*.md" -not -path "./.git/*" -not -path "./node_modules/*" -print0) + exit $errors + + - name: Validate YAML code blocks + run: | + errors=0 + while IFS= read -r -d '' file; do + python3 -c " + import re, sys, yaml + content = open('$file').read() + blocks = re.findall(r'\`\`\`yaml\n(.*?)\`\`\`', content, re.DOTALL) + for i, block in enumerate(blocks): + try: + yaml.safe_load(block) + except yaml.YAMLError as e: + print(f' ERROR in $file YAML block {i+1}: {e}') + sys.exit(1) + if blocks: + print(f' OK: $file — {len(blocks)} YAML blocks') + " || errors=$((errors + 1)) + done < <(find . -name "*.md" -not -path "./.git/*" -not -path "./node_modules/*" -print0) + exit $errors + + - name: Check all variants exist + run: | + for variant in DDEV Vanilla Lagoon; do + if [ ! -f "$variant/AGENTS.md" ]; then + echo "ERROR: Missing $variant/AGENTS.md" + exit 1 + fi + lines=$(wc -l < "$variant/AGENTS.md") + echo "OK: $variant/AGENTS.md ($lines lines)" + done diff --git a/.gitignore b/.gitignore index adc82e1..7acc910 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,13 @@ # Drupal AGENTS.md Project .gitignore .claude +.cursor +.DS_Store +*.swp +*.swo +*~ +.vscode/ +.idea/ +node_modules/ +package-lock.json +.pi diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..c8f8a64 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,15 @@ +{ + "default": true, + "MD013": false, + "MD033": false, + "MD034": false, + "MD036": false, + "MD040": false, + "MD041": false, + "MD051": false, + "MD060": false, + "MD025": { "front_matter_title": "title" }, + "MD022": false, + "MD031": false, + "MD032": false +} diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1 @@ +node_modules/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +node_modules diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..033c32d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "proseWrap": "never", + "printWidth": 120 +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b478705 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,63 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +## [Unreleased] — 2025-04-23 + +### Added + +- **Lagoon/AGENTS.md**: New variant for amazee.io Lagoon (Kubernetes-based hosting) + - Lagoon CLI commands and configuration + - lagoon-sync for database and file synchronization + - Environment variables (`LAGOON_PROJECT`, `LAGOON_ENVIRONMENT_TYPE`, etc.) + - Drush alias integration for remote operations + - Post-rollout task configuration + - Redis and Varnish configuration for Lagoon +- **CONTRIBUTING.md**: Structured contributing guide with PR template and review criteria +- **CHANGELOG.md**: This file +- **CI workflow**: GitHub Actions for markdown linting, YAML validation, and link checking +- **Internal table of contents** in all AGENTS.md variants for quick navigation +- **Module scaffolding template**: Full file structure with minimal module files (info.yml, composer.json) +- **Anti-patterns section**: 14 "Never Do This" guidelines across all variants +- **Concrete code examples** for all major patterns: + - Services with dependency injection (YAML + PHP) + - Entity queries with `accessCheck(TRUE)` + - Block plugin with annotations + - Hooks: `hook_form_alter`, `hook_theme`, `hook_entity_presave`, `hook_cron` + - Forms: simple form with AJAX + config form + - Routes with custom access checker + - EventSubscriber with service tag + - Batch API with `BatchBuilder` + - QueueWorker plugin + - Render API with caching + - Migration source/process/destination + - Drupal behaviors (JavaScript) + - Configuration schema and install files +- **New development topics** across all variants: + - Events & EventSubscribers + - Configuration Management (schema, install, optional, config split) + - Render API deep dive + - Migration API with custom process plugin + - Composer management best practices + - JavaScript & Drupal behaviors + - Content Moderation & Workflows +- **Complete test examples**: + - Unit test with Prophecy mocking + - Kernel test with entity schema + - Functional test with browser assertions +- **Version compatibility table** in README + +### Changed + +- Updated PHP requirement from 8.1+ to **8.3+** +- Updated Drupal version from "10.x+" to **"10.x / 11.x"** +- Updated Drush version from 12+ to **13+** +- DDEV config updated to use PHP 8.3 +- README updated with Lagoon variant, version compatibility table, and improved structure + +### Fixed + +- Fixed broken markdown code fence in Vanilla/AGENTS.md (Performance Issues section) +- Expanded `.gitignore` with `.cursor`, `.DS_Store`, and `*.swp` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0156ab1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,141 @@ +# Contributing to Drupal AGENTS.md + +Thank you for your interest in improving the Drupal AI Agent Development Guides! This document provides guidelines and instructions for contributing. + +## How to Contribute + +### Reporting Issues + +- Open a [GitHub Issue](https://github.com/amazeeio/drupal-agents-md/issues) with a clear title and description +- Specify which AGENTS.md variant is affected (DDEV, Vanilla, or Lagoon) +- Include the section heading where the issue occurs +- If suggesting a change, explain **why** the current content is incorrect or incomplete + +### Making Changes + +1. **Fork the repository** and create a feature branch: + + ```bash + git checkout -b feature/my-improvement + ``` + +2. **Make your changes** following the guidelines below + +3. **Test your changes** — Verify that: + - Markdown renders correctly (no broken fences, tables, or links) + - Code examples are syntactically valid PHP/YAML/Twig/JavaScript + - Commands are accurate for the target environment + - Content follows the existing structure and tone + +4. **Commit with a descriptive message**: + + ```bash + git commit -m "feat: add Recipe system section to all variants" + ``` + +5. **Open a Pull Request** against the `main` branch + +## Commit Message Convention + +Follow [Conventional Commits](https://www.conventionalcommits.org/): + +- `feat:` New content or sections +- `fix:` Corrections to existing content +- `docs:` README, CONTRIBUTING, or meta-documentation changes +- `refactor:` Restructure without changing content +- `chore:` CI, gitignore, or tooling changes + +## Content Guidelines + +### Code Examples + +- All code examples must be **syntactically valid** and **copy-pasteable** +- Use `my_module` as the placeholder module name +- Follow Drupal coding standards in all PHP examples +- Include `use` statements for all referenced classes +- Show dependency injection patterns, not static calls + +### Environment-Specific Content + +When adding content that applies to all variants: + +- Add it to **all three** files: DDEV, Vanilla, and Lagoon +- Adapt commands to the environment: + - DDEV: `ddev exec drush ` + - Vanilla: `drush ` + - Lagoon: `drush @lagoon. ` or `lagoon-sync` commands +- Maintain consistency across variants + +### Style + +- Use `**bold**` for emphasis on key terms +- Use fenced code blocks with language identifiers (`php,`yaml, ```bash) +- Use markdown tables for command references +- Keep paragraphs concise — AI agents benefit from density over prose +- Add concrete code examples rather than abstract descriptions + +### What to Add + +Contributions are especially welcome for: + +- **New Drupal patterns** — Recipe system, Typed Data, new plugin types +- **More code examples** — Real-world patterns that agents frequently need +- **Drupal version-specific notes** — Changes between Drupal 10 and 11 +- **Performance patterns** — Profiling, optimization, and caching +- **Security patterns** — Common vulnerability prevention +- **Testing patterns** — More test type examples (FunctionalJavascript, etc.) +- **Decoupled/headless** — JSON:API, REST, GraphQL patterns + +### What Not to Add + +- Basic Drupal tutorials (installation, site building) +- Server infrastructure guides (Apache/Nginx config) +- Content that duplicates the official Drupal documentation without adding value +- Drupal 7, 8, or 9 specific guidance + +## Pull Request Template + +When opening a PR, please include: + +```markdown +## Description + +Brief description of what this PR changes and why. + +## Affected Files + +- [ ] DDEV/AGENTS.md +- [ ] Vanilla/AGENTS.md +- [ ] Lagoon/AGENTS.md +- [ ] README.md +- [ ] Other: \_\_\_ + +## Type of Change + +- [ ] New content/section +- [ ] Correction/fix +- [ ] Code example addition +- [ ] Documentation/meta +- [ ] CI/tooling + +## Testing + +How did you verify the changes? + +- [ ] Rendered markdown preview +- [ ] Checked code syntax +- [ ] Verified commands work in target environment +``` + +## Review Criteria + +PRs will be reviewed against: + +1. **Accuracy** — Code examples and commands are correct +2. **Consistency** — Changes applied to all relevant variants +3. **Style** — Follows existing structure and formatting conventions +4. **Value** — Adds useful information for AI agents working on Drupal projects + +## Questions? + +Open an issue with the `question` label, or start a discussion in GitHub Discussions. diff --git a/DDEV/AGENTS.md b/DDEV/AGENTS.md index e453ee1..2f014cb 100644 --- a/DDEV/AGENTS.md +++ b/DDEV/AGENTS.md @@ -2,17 +2,35 @@ **AI Agent Instructions**: This guide provides comprehensive instructions for AI coding agents working on Drupal projects using DDEV. Follow these guidelines for consistent, high-quality contributions. Human contributors should use README.md instead. +## Table of Contents + +- [Project Overview](#project-overview) +- [DDEV Quick Setup](#ddev-quick-setup) +- [Module Scaffolding Template](#module-scaffolding-template) +- [Code Style and Standards](#code-style-and-standards) +- [Drupal Development Patterns](#drupal-development-patterns) +- [Security & Performance Guidelines](#security--performance-guidelines) +- [Anti-Patterns — Never Do This](#anti-patterns--never-do-this) +- [Testing & Quality Assurance](#testing--quality-assurance) +- [DDEV Development Workflow](#ddev-development-workflow) +- [Advanced Development Patterns](#advanced-development-patterns) +- [Additional Topics](#additional-topics) +- [Troubleshooting](#troubleshooting) +- [Additional Resources](#additional-resources) + ## Project Overview -- **Core Technology**: Drupal 10.x+ (verify via `composer show drupal/core`) + +- **Core Technology**: Drupal 10.x / 11.x (verify via `ddev exec composer show drupal/core`) - **Development Environment**: DDEV (Docker-based development environment) - **Key Components**: Custom modules, themes, configuration management, Composer dependencies -- **Environment**: PHP 8.1+, MySQL/MariaDB, Nginx (all managed by DDEV) -- **Development Tools**: Composer, Drush 12+, Git, DDEV CLI +- **Environment**: PHP 8.3+, MySQL/MariaDB, Nginx (all managed by DDEV) +- **Development Tools**: Composer, Drush 13+, Git, DDEV CLI - **Important**: All DDEV commands should be run from project root. Use `ddev exec` for Drupal-specific commands. ## DDEV Quick Setup ### Prerequisites + ```bash # Install DDEV (macOS) brew install ddev/ddev/ddev @@ -23,13 +41,14 @@ ddev --version ``` ### Initialize DDEV Project + ```bash # Clone the repository git clone my-drupal-project cd my-drupal-project # Initialize DDEV configuration -ddev config --project-type=drupal --docroot=web --php-version=8.1 +ddev config --project-type=drupal --docroot=web --php-version=8.3 # Start DDEV environment ddev start @@ -55,6 +74,7 @@ ddev launch ``` ### Essential DDEV Commands + ```bash # Environment management ddev start # Start development environment @@ -77,13 +97,14 @@ ddev launch # Open site in browser ``` ### DDEV Configuration + Create `.ddev/config.yaml` for project-specific settings: ```yaml # .ddev/config.yaml type: drupal docroot: web -php_version: "8.1" +php_version: "8.3" webserver_type: nginx-fpm router_http_port: "80" router_https_port: "443" @@ -97,7 +118,109 @@ web_environment: - DRUSH_OPTIONS_URI=https://my-drupal-project.ddev.site ``` +## Module Scaffolding Template + +When creating a new custom module, follow this structure: + +``` +web/modules/custom/my_module/ +├── my_module.info.yml # Module metadata (required) +├── my_module.module # Hook implementations +├── my_module.routing.yml # Route definitions +├── my_module.services.yml # Service definitions +├── my_module.permissions.yml # Permission definitions +├── my_module.links.menu.yml # Menu links +├── my_module.links.action.yml # Action links +├── my_module.links.task.yml # Task (tab) links +├── my_module.libraries.yml # CSS/JS libraries +├── my_module.routing.yml # Route definitions +├── composer.json # PSR-4 autoloading +├── src/ +│ ├── Controller/ +│ │ └── MyController.php +│ ├── Form/ +│ │ ├── SettingsForm.php # ConfigFormBase +│ │ └── CustomForm.php # FormBase +│ ├── Plugin/ +│ │ ├── Block/ +│ │ │ └── MyBlock.php +│ │ ├── Field/ +│ │ │ ├── FieldFormatter/ +│ │ │ │ └── MyFormatter.php +│ │ │ ├── FieldWidget/ +│ │ │ │ └── MyWidget.php +│ │ │ └── FieldType/ +│ │ │ └── MyFieldItem.php +│ │ └── QueueWorker/ +│ │ └── MyQueueWorker.php +│ ├── EventSubscriber/ +│ │ └── MyEventSubscriber.php +│ ├── Access/ +│ │ └── MyAccessChecker.php +│ ├── Entity/ +│ │ └── MyEntity.php +│ └── Service/ +│ └── MyService.php +├── config/ +│ ├── install/ # Config installed with module +│ │ └── my_module.settings.yml +│ ├── optional/ # Config installed only if dependencies met +│ │ └── field.field.node.article.field_my_field.yml +│ └── schema/ # Config schema for typed data +│ └── my_module.schema.yml +├── templates/ +│ └── my-module-template.html.twig +├── css/ +│ └── my-module.css +├── js/ +│ └── my-module.js +└── tests/ + └── src/ + ├── Unit/ + │ └── MyServiceTest.php + ├── Kernel/ + │ └── MyModuleKernelTest.php + └── Functional/ + └── MyModuleFunctionalTest.php +``` + +### Minimal Module Files + +**my_module.info.yml**: + +```yaml +name: "My Module" +type: module +description: "Custom module description." +core_version_requirement: ^10 || ^11 +package: Custom +dependencies: + - drupal:node + - drupal:user +``` + +**composer.json** (for PSR-4 autoloading in tests): + +```json +{ + "name": "drupal/my_module", + "type": "drupal-custom-module", + "description": "Custom module description.", + "autoload": { + "psr-4": { + "Drupal\\my_module\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Drupal\\Tests\\my_module\\": "tests/src/" + } + } +} +``` + ## Code Style and Standards + Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and PHPCS for enforcement. - **PHP**: @@ -111,6 +234,7 @@ Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and - **Twig**: `{{ }}` for output, `{% %}` for logic; always escape with `|e` - **Linting**: + ```bash ddev exec vendor/bin/phpcs --standard=Drupal --extensions=php,inc,module,install,info,yml src/ ddev exec vendor/bin/phpcs --standard=DrupalPractice --extensions=php,inc,module,install,info,yml src/ @@ -122,65 +246,458 @@ Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and ## Drupal Development Patterns ### Services & Dependency Injection -- **Create services** in `modulename.services.yml` file for reusable logic -- **Use dependency injection** to inject services into controllers, forms, and plugins -- **Core services** like `@current_user`, `@entity_type.manager`, `@database` are available + +**Create services** in `modulename.services.yml`: + +```yaml +# my_module.services.yml +services: + my_module.my_service: + class: Drupal\my_module\Service\MyService + arguments: ["@entity_type.manager", "@logger.factory", "@config.factory"] + tags: + - { name: backend_overridable } +``` + +**Use dependency injection** in controllers, forms, and plugins: + +```php +namespace Drupal\my_module\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +class MyController extends ControllerBase { + + public function __construct( + protected EntityTypeManagerInterface $entityTypeManager, + ) {} + + public static function create(ContainerInterface $container): static { + return new static( + $container->get('entity_type.manager'), + ); + } + + public function content(): array { + $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple(); + // ... + return $build; + } +} +``` + +- **Core services** like `@current_user`, `@entity_type.manager`, `@database`, `@config.factory`, `@logger.factory` are available - **Best practice**: Avoid static `\Drupal::` calls in favor of dependency injection -- **Service discovery**: Use `drush eval "print_r(\Drupal::getContainer()->getServiceIds());"` to see available services +- **Service discovery**: Use `ddev exec drush php:eval "print_r(\Drupal::getContainer()->getServiceIds());"` to see available services - **Location**: Place service classes in `src/` directory with proper namespace ### Entity API & Queries -- **Entity loading**: Use `Entity::load($id)` for single entities or `entityTypeManager()->getStorage()` for multiple -- **Entity queries**: Use `\Drupal::entityQuery()` for database operations instead of raw SQL -- **Query conditions**: Chain multiple conditions with `->condition()`, `->sort()`, `->range()` -- **Entity creation**: Create entities with `Entity::create(['type' => 'bundle_name'])` -- **Field access**: Use entity field API instead of direct property access -- **Performance**: Use entity query cache tags and contexts for optimal caching + +**Loading entities**: + +```php +// Single entity +$node = \Drupal::entityTypeManager()->getStorage('node')->load(123); + +// Multiple entities +$nodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple([1, 2, 3]); + +// Load by properties +$nodes = \Drupal::entityTypeManager()->getStorage('node')->loadByProperties([ + 'type' => 'article', + 'status' => 1, +]); +``` + +**Entity queries** (always prefer over raw SQL): + +```php +use Drupal\Core\Entity\Query\QueryInterface; + +// Modern entity query with injected service +$ids = $this->entityTypeManager->getStorage('node')->getQuery() + ->condition('type', 'article') + ->condition('status', 1) + ->condition('field_category', $categoryId) + ->sort('created', 'DESC') + ->range(0, 10) + ->accessCheck(TRUE) // ALWAYS set explicitly in Drupal 10.2+ + ->execute(); + +$nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($ids); +``` + +**Creating entities**: + +```php +$node = \Drupal::entityTypeManager()->getStorage('node')->create([ + 'type' => 'article', + 'title' => 'My Article', + 'body' => [ + 'value' => 'Content here', + 'format' => 'full_html', + ], + 'status' => 1, + 'uid' => 1, +]); +$node->save(); +``` + +**Field access**: Use entity field API instead of direct property access: + +```php +// Correct +$node->get('field_my_field')->value; +$node->get('field_my_field')->entity; // For entity reference fields + +// Avoid +$node->field_my_field->value; // Magic __get — works but less explicit +``` ### Plugin System -- **Plugin types**: Blocks, field formatters, field widgets, menu links, and more -- **Plugin discovery**: Use annotation-based discovery in docblocks -- **Plugin configuration**: Define plugin ID, label, and other metadata in annotations -- **Plugin base classes**: Extend appropriate base classes (BlockBase, FormatterBase, etc.) + +**Block plugin example**: + +```php +namespace Drupal\my_module\Plugin\Block; + +use Drupal\Core\Block\BlockBase; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides a 'My Custom Block' block. + * + * @Block( + * id = "my_custom_block", + * admin_label = @Translation("My Custom Block"), + * category = @Translation("Custom"), + * context_definitions = { + * "node" = @ContextDefinition("entity:node", label = @Translation("Node")) + * } + * ) + */ +class MyCustomBlock extends BlockBase implements ContainerFactoryPluginInterface { + + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { + return new static($configuration, $plugin_id, $plugin_definition); + } + + public function build(): array { + return [ + '#markup' => $this->t('Hello from my custom block!'), + '#cache' => [ + 'tags' => ['node_list'], + 'contexts' => ['user.roles'], + ], + ]; + } +} +``` + +- **Plugin types**: Blocks, field formatters, field widgets, field types, menu links, QueueWorker, Condition, Action, and more +- **Plugin discovery**: Use annotation-based discovery (as shown above) or YAML discovery +- **Plugin base classes**: Extend appropriate base classes (`BlockBase`, `FormatterBase`, `WidgetBase`, etc.) - **Plugin placement**: Place plugins in `src/Plugin/Type/` directory structure -- **Derivative plugins**: Use for creating multiple plugins from one definition +- **Derivative plugins**: Use `DeriverBase` for creating multiple plugins from one definition ### Hooks -- **Hook implementation**: Implement hooks in `modulename.module` file + +**Implement hooks** in `modulename.module` file: + +```php +/** + * Implements hook_form_alter(). + */ +function my_module_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id): void { + if ($form_id === 'node_article_form') { + $form['title']['#title'] = t('Article Title'); + $form['actions']['submit']['#value'] = t('Publish Article'); + } +} + +/** + * Implements hook_theme(). + */ +function my_module_theme($existing, $type, $theme, $path): array { + return [ + 'my_template' => [ + 'variables' => [ + 'title' => '', + 'items' => [], + ], + 'template' => 'my-template', + ], + ]; +} + +/** + * Implements hook_entity_presave(). + */ +function my_module_entity_presave(\Drupal\Core\Entity\EntityInterface $entity): void { + if ($entity->getEntityTypeId() === 'node' && $entity->bundle() === 'article') { + // Auto-set a field before saving. + $entity->set('field_last_updated', \Drupal::time()->getRequestTime()); + } +} + +/** + * Implements hook_cron(). + */ +function my_module_cron(): void { + // Process items during cron runs. + \Drupal::queue('my_module_processor')->createItem(['type' => 'cleanup']); +} +``` + - **Hook naming**: Follow pattern `hook_modulename_action()` for custom hooks - **Hook parameters**: Use type hints and proper parameter documentation -- **Core hooks**: Common hooks include `hook_form_alter()`, `hook_theme()`, `hook_menu_links_discovered_alter()` +- **Core hooks**: Common hooks include `hook_form_alter()`, `hook_theme()`, `hook_entity_presave()`, `hook_cron()`, `hook_menu_links_discovered_alter()` - **Hook order**: Hooks fire in module weight order (lowest first) -- **Best practice**: Keep hook implementations focused and use services for complex logic +- **Best practice**: Keep hook implementations focused and delegate complex logic to services ### Forms API -- **Form classes**: Extend `FormBase` for simple forms or `ConfigFormBase` for configuration forms -- **Form structure**: Use render array structure with `#type`, `#title`, `#description` properties -- **Form validation**: Implement `validateForm()` method for custom validation -- **Form submission**: Implement `submitForm()` method for processing form data -- **Form elements**: Use proper form element types (textfield, select, checkbox, etc.) -- **AJAX forms**: Add `#ajax` property to form elements for dynamic behavior -- **Form caching**: Forms are automatically cached with CSRF protection + +**Simple form**: + +```php +namespace Drupal\my_module\Form; + +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; + +class CustomForm extends FormBase { + + public function getFormId(): string { + return 'my_module_custom_form'; + } + + public function buildForm(array $form, FormStateInterface $form_state): array { + $form['email'] = [ + '#type' => 'email', + '#title' => $this->t('Email Address'), + '#required' => TRUE, + ]; + + $form['category'] = [ + '#type' => 'select', + '#title' => $this->t('Category'), + '#options' => [ + 'news' => $this->t('News'), + 'events' => $this->t('Events'), + 'blog' => $this->t('Blog'), + ], + '#ajax' => [ + 'callback' => '::categoryChanged', + 'wrapper' => 'subcategory-wrapper', + 'event' => 'change', + ], + ]; + + $form['subcategory'] = [ + '#type' => 'container', + '#attributes' => ['id' => 'subcategory-wrapper'], + 'value' => [ + '#type' => 'textfield', + '#title' => $this->t('Subcategory'), + ], + ]; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + public function categoryChanged(array &$form, FormStateInterface $form_state): array { + return $form['subcategory']; + } + + public function validateForm(array &$form, FormStateInterface $form_state): void { + $email = $form_state->getValue('email'); + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + $form_state->setErrorByName('email', $this->t('Please enter a valid email address.')); + } + } + + public function submitForm(array &$form, FormStateInterface $form_state): void { + $this->messenger()->addStatus($this->t('Form submitted successfully.')); + $form_state->setRedirect(''); + } +} +``` + +**Configuration form**: + +```php +namespace Drupal\my_module\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; + +class SettingsForm extends ConfigFormBase { + + public function getFormId(): string { + return 'my_module_settings'; + } + + protected function getEditableConfigNames(): array { + return ['my_module.settings']; + } + + public function buildForm(array $form, FormStateInterface $form_state): array { + $config = $this->config('my_module.settings'); + + $form['api_key'] = [ + '#type' => 'textfield', + '#title' => $this->t('API Key'), + '#default_value' => $config->get('api_key'), + '#required' => TRUE, + ]; + + $form['max_items'] = [ + '#type' => 'number', + '#title' => $this->t('Maximum Items'), + '#default_value' => $config->get('max_items') ?? 50, + '#min' => 1, + '#max' => 500, + ]; + + return parent::buildForm($form, $form_state); + } + + public function submitForm(array &$form, FormStateInterface $form_state): void { + $this->config('my_module.settings') + ->set('api_key', $form_state->getValue('api_key')) + ->set('max_items', $form_state->getValue('max_items')) + ->save(); + + parent::submitForm($form, $form_state); + } +} +``` ### Routes & Controllers -- **Routing file**: Define routes in `modulename.routing.yml` with path, defaults, and requirements -- **Controllers**: Create controller classes extending `ControllerBase` in `src/Controller/` -- **Route parameters**: Use `{parameter}` placeholders in paths and inject into controller methods -- **Access control**: Implement `_permission`, `_role`, or custom access callbacks -- **Route naming**: Use `modulename.action` naming convention for clarity -- **Controller injection**: Use constructor injection for dependencies -- **Return values**: Return render arrays or Symfony Response objects + +**Route definition** (`my_module.routing.yml`): + +```yaml +my_module.content: + path: "/my-module/{node}" + defaults: + _controller: '\Drupal\my_module\Controller\MyController::content' + _title: "My Module Page" + requirements: + _permission: "access content" + node: \d+ + +my_module.settings: + path: "/admin/config/my-module/settings" + defaults: + _form: '\Drupal\my_module\Form\SettingsForm' + _title: "My Module Settings" + requirements: + _permission: "administer site configuration" + +my_module.custom_access: + path: "/my-module/custom/{node}" + defaults: + _controller: '\Drupal\my_module\Controller\MyController::customPage' + _title_callback: '\Drupal\my_module\Controller\MyController::pageTitle' + requirements: + _custom_access: '\Drupal\my_module\Access\MyAccessChecker::access' +``` + +**Controller**: + +```php +namespace Drupal\my_module\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\node\NodeInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +class MyController extends ControllerBase { + + public function __construct( + protected EntityTypeManagerInterface $entityTypeManager, + ) {} + + public static function create(ContainerInterface $container): static { + return new static( + $container->get('entity_type.manager'), + ); + } + + public function content(NodeInterface $node): array { + return [ + '#theme' => 'my_template', + '#title' => $node->label(), + '#items' => $this->getItems($node), + '#cache' => [ + 'tags' => ['node:' . $node->id()], + 'contexts' => ['user.permissions'], + ], + '#attached' => [ + 'library' => ['my_module/my_module.styles'], + ], + ]; + } + + public function pageTitle(NodeInterface $node): string { + return $this->t('Page: @title', ['@title' => $node->label()]); + } +} +``` + +**Custom access checker**: + +```php +namespace Drupal\my_module\Access; + +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Routing\Access\AccessInterface; +use Drupal\node\NodeInterface; + +class MyAccessChecker implements AccessInterface { + + public function access(NodeInterface $node): AccessResult { + return AccessResult::allowedIf($node->isPublished()) + ->addCacheTags(['node:' . $node->id()]) + ->cachePerUser(); + } +} +``` + +Register in `my_module.services.yml`: + +```yaml +my_module.access_checker: + class: Drupal\my_module\Access\MyAccessChecker + tags: + - { name: access_check } +``` ## Security & Performance Guidelines ### Security Requirements + - **Always sanitize user input**: Use `#plain_text` for untrusted content - **CSRF protection**: Include `#token` for forms with side effects - **Permissions**: Implement proper access checks and route requirements - **SQL Injection**: Use Entity Query or proper parameter binding - **XSS Prevention**: Always use `|e` filter in Twig, `#markup` for trusted HTML only +- **File uploads**: Validate file types and sizes; use Drupal's file API +- **Database credentials**: Never commit credentials to version control +- **Render arrays**: Never use `#markup` with unsanitized user input; use `#plain_text` or `check_plain()` ### Performance Best Practices + - **Render caching**: Always add `#cache` array to render arrays with appropriate `tags` and `contexts` - **Cache tags**: Use entity-based tags like `['node:123']` or list-based tags like `['node_list']` - **Cache contexts**: Apply user-specific contexts like `['user.roles']` for personalized content @@ -189,24 +706,254 @@ Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and - **Cache max-age**: Set appropriate `max-age` values based on content freshness requirements - **Avoid premature optimization**: Profile first, then optimize based on actual bottlenecks - **Database queries**: Use entity queries instead of raw SQL for better caching and security -- **Entity loading**: Load multiple entities at once when possible for better performance +- **Entity loading**: Load multiple entities at once with `loadMultiple()` instead of individual loads + +**Render array with caching**: + +```php +$build = [ + '#theme' => 'item_list', + '#items' => $items, + '#cache' => [ + 'keys' => ['my_module:item_list:' . $categoryId], + 'tags' => ['node_list', 'taxonomy_term:' . $categoryId], + 'contexts' => ['user.roles'], + 'max-age' => 3600, + ], +]; +``` + +**Lazy builder for expensive operations**: + +```php +$build['expensive_content'] = [ + '#lazy_builder' => [ + '\Drupal\my_module\Service\MyLazyBuilder::renderExpensiveContent', + [$param1, $param2], + ], + '#create_placeholder' => TRUE, +]; +``` ### Caching Strategies + - **Render cache**: Cache complex markup with proper tags/contexts -- **Dynamic page cache**: Configure for anonymous users -- **Internal page cache**: Enable for authenticated users -- **Entity cache**: Leverage core entity caching -- **Redis/Memcache**: Configure for distributed caching +- **Dynamic page cache**: Automatically handles cacheability for anonymous users +- **Internal page cache**: Serves full cached pages for anonymous users +- **Entity cache**: Core entity caching is automatic — invalidate with cache tags +- **Redis/Memcache**: Configure for distributed caching in production + +## Anti-Patterns — Never Do This + +These are common mistakes that an AI agent must avoid: + +1. **Never use `\Drupal::` static calls in services, controllers, or plugins** — Use dependency injection instead. The only acceptable use is in `hook_` functions in `.module` files (and even there, consider delegating to a service). + +2. **Never query the database directly when Entity Query suffices** — Use `\Drupal::entityQuery()` or injected `$this->entityTypeManager->getStorage()->getQuery()`. + +3. **Never use `|raw` in Twig** — Use `|e` (or rely on auto-escaping). If you need raw HTML, use `#type => 'processed_text'` or `check_markup()`. + +4. **Never create monolithic `hook_form_alter()` functions** — If the alter logic is complex, delegate to a service. Break large hooks into focused helper methods. + +5. **Never store configuration in state that belongs in config** — State (`\Drupal::state()`) is for ephemeral/transient data (last cron run, temporary flags). Config (`\Drupal::configFactory()`) is for structured, exportable settings. + +6. **Never use `#markup` with unsanitized user input** — Always use `#plain_text` for untrusted content or `check_plain()` / `Html::escape()` for escaping. + +7. **Never skip `accessCheck(TRUE)` on entity queries** — Drupal 10.2+ requires explicit access checking on entity queries. Omitting it throws deprecation warnings and will be required in Drupal 12. + +8. **Never hardcode entity IDs, user IDs, or paths** — Use configuration, route names, and dynamic lookups instead. + +9. **Never use `hook_views_data()` without proper table aliases** — Always prefix table columns clearly to avoid SQL ambiguity. + +10. **Never ignore cacheability metadata** — Every render array that depends on data must specify `#cache` tags, contexts, and max-age. Missing cache metadata causes stale content or unnecessary cache invalidation. + +11. **Never commit `settings.php` with database credentials** — Use environment variables or `settings.local.php` (excluded from VCS). + +12. **Never use `node_load()` or other deprecated procedural functions** — Use the entity type manager: `\Drupal::entityTypeManager()->getStorage('node')->load()`. + +13. **Never use global variables like `$_GET`, `$_POST`, `$_SERVER`** — Use Symfony's `Request` object via dependency injection. + +14. **Never put business logic in `.module` files** — Delegate to services. The `.module` file should be thin: route hooks, theme hooks, and thin wrappers that call services. + +## Testing & Quality Assurance + +### PHPUnit Testing Framework + +Aim for ≥ 80% code coverage. Drupal provides multiple test types: + +```bash +# Run all tests with coverage +ddev exec vendor/bin/phpunit -v --coverage-html coverage/ + +# Run specific test suites +ddev exec vendor/bin/phpunit --testsuite unit # Unit tests (fast) +ddev exec vendor/bin/phpunit --testsuite kernel # Kernel tests +ddev exec vendor/bin/phpunit --testsuite functional # Functional tests (slower) +ddev exec vendor/bin/phpunit --testsuite javascript # JavaScript tests + +# Run specific tests +ddev exec vendor/bin/phpunit --filter MyModuleUnitTest +ddev exec vendor/bin/phpunit web/modules/custom/my_module/tests/src/Unit/ + +# Run with custom configuration +SIMPLETEST_DB=sqlite://localhost/tmp.sqlite ddev exec vendor/bin/phpunit +``` + +### Unit Test Example + +```php +// tests/src/Unit/MyServiceTest.php +namespace Drupal\Tests\my_module\Unit; + +use Drupal\Tests\UnitTestCase; +use Drupal\my_module\Service\MyService; +use Prophecy\PhpUnit\ProphecyTrait; + +class MyServiceTest extends UnitTestCase { + use ProphecyTrait; + + public function testProcessReturnsExpectedValue(): void { + $logger = $this->prophesize('\Psr\Log\LoggerInterface'); + $service = new MyService($logger->reveal()); + $result = $service->process('input'); + $this->assertEquals('expected_output', $result); + } + + public function testProcessThrowsOnEmptyInput(): void { + $logger = $this->prophesize('\Psr\Log\LoggerInterface'); + $service = new MyService($logger->reveal()); + $this->expectException(\InvalidArgumentException::class); + $service->process(''); + } +} +``` + +### Kernel Test Example + +```php +// tests/src/Kernel/MyModuleKernelTest.php +namespace Drupal\Tests\my_module\Kernel; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\node\Entity\Node; +use Drupal\node\Entity\NodeType; + +class MyModuleKernelTest extends KernelTestBase { + + protected static $modules = ['system', 'node', 'user', 'my_module']; + + protected function setUp(): void { + parent::setUp(); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installConfig(['my_module']); + + // Create a node type for testing. + NodeType::create(['type' => 'article', 'name' => 'Article'])->save(); + } + + public function testNodeCreation(): void { + $node = Node::create([ + 'type' => 'article', + 'title' => 'Test Article', + 'status' => 1, + ]); + $node->save(); + + $this->assertNotNull($node->id()); + $this->assertEquals('article', $node->bundle()); + } +} +``` + +### Functional Test Example + +```php +// tests/src/Functional/MyModuleFunctionalTest.php +namespace Drupal\Tests\my_module\Functional; + +use Drupal\Tests\BrowserTestBase; + +class MyModuleFunctionalTest extends BrowserTestBase { + + protected $defaultTheme = 'stark'; + protected static $modules = ['node', 'my_module']; + + protected function setUp(): void { + parent::setUp(); + // Create a test user with permissions. + $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + $user = $this->drupalCreateUser(['access content', 'create article content']); + $this->drupalLogin($user); + } + + public function testArticleCreation(): void { + $this->drupalGet('/node/add/article'); + $this->assertSession()->statusCodeEquals(200); + + // Submit the node form. + $edit = [ + 'title[0][value]' => 'Test Article Title', + ]; + $this->submitForm($edit, 'Save'); + $this->assertSession()->pageTextContains('Article Test Article Title has been created.'); + } + + public function testMyModulePageAccess(): void { + // Anonymous users should not access custom pages. + $this->drupalGet('/my-module/custom/1'); + $this->assertSession()->statusCodeEquals(403); + } +} +``` + +### Code Quality Tools in DDEV + +```bash +# Static analysis (add to composer require) +ddev exec vendor/bin/phpstan analyse # PHPStan analysis +ddev exec vendor/bin/psalm # Psalm analysis + +# Security scanning +ddev exec vendor/bin/drupal-check # Check for deprecated code +ddev exec composer audit # Check for security advisories + +# Accessibility testing +ddev exec vendor/bin/phpunit --group accessibility # Accessibility tests +``` + +### JavaScript Testing + +```bash +# Install JavaScript dependencies +ddev exec npm install + +# Run JavaScript tests +ddev exec npm run test # Jest tests +ddev exec npm run test:a11y # Accessibility tests +``` + +### Before Submitting Code + +```bash +# Quality checklist +ddev exec vendor/bin/phpcs --standard=Drupal . # Code style +ddev exec vendor/bin/phpunit # Run tests +ddev exec drush cr # Clear caches +ddev exec drush updatedb # Run updates +``` ## DDEV Development Workflow ### Project Structure + - **Modules** → `web/modules/custom/` - **Themes** → `web/themes/custom/` - **Configuration** → Export with `ddev exec drush config:export` - **Profiles** → `web/profiles/custom/` ### Essential Development Commands + ```bash # Cache management (run inside DDEV) ddev exec drush cr # Clear all caches @@ -224,65 +971,53 @@ ddev exec drush updatedb # Run database updates ### Debugging in DDEV #### Core Debugging & Information Commands -| Command | Purpose | Why it's useful for debugging | -|----------------------------------|-------------------------------------------------------------------------|--------------------------------------------------------------------| -| `ddev exec drush status` | Shows Drupal root, site path, database connection, Drush version, etc. | Quickly verify that DDEV is pointing to the correct site and DB is connected. | -| `ddev exec drush core-status` | Same as above but more detailed in newer versions. | | -| `ddev exec drush watchdog:show` | Lists recent log messages (dblog entries). | Primary command to read the Drupal error/log messages without going to /admin/reports/dblog. Supports filters: `--severity=Error`, `--type=php`, etc. | -| `ddev exec drush watchdog:delete all` | Clears the watchdog log. | Useful when logs become huge and slow down watchdog operations. | -| `ddev exec drush sql:query "SELECT * FROM watchdog ORDER BY wid DESC LIMIT 50"` | Direct SQL access to logs when the database is very large. | Faster than watchdog:show on sites with millions of log entries. | + +| Command | Purpose | +| --- | --- | +| `ddev exec drush status` | Shows Drupal root, site path, database connection, Drush version | +| `ddev exec drush watchdog:show` | Lists recent log messages (dblog entries). Filters: `--severity=Error` | +| `ddev exec drush watchdog:delete all` | Clears the watchdog log | +| `ddev exec drush sql:query "SELECT * FROM watchdog ORDER BY wid DESC LIMIT 50"` | Direct SQL access to logs | #### Cache Debugging -| Command | Purpose | -|----------------------------------|-------------------------------------------------------------------------| -| `ddev exec drush cache:rebuild` | Rebuilds all caches (equivalent to "drush cc all" in D7). | -| `ddev exec drush cr` | Alternative cache rebuild command. | -| `ddev exec drush cache:get :` | Retrieve a specific cache item (e.g., `ddev exec drush cache:get config:core.extension`). | -| `ddev exec drush cache:clear ` | Clear only one cache bin (render, config, discovery, etc.). | + +| Command | Purpose | +| --------------------------------------- | ------------------------------ | +| `ddev exec drush cache:rebuild` | Rebuilds all caches | +| `ddev exec drush cache:get :` | Retrieve a specific cache item | +| `ddev exec drush cache:clear ` | Clear only one cache bin | #### Configuration Debugging -| Command | Purpose | -|----------------------------------------------|-------------------------------------------------------------------------| -| `ddev exec drush config:get ` | Show a single configuration value (e.g., `ddev exec drush config:get system.site`). | -| `ddev exec drush config:set ` | Temporarily change a config value without using the UI. | -| `ddev exec drush config:export` | Export active config to sync directory. | -| `ddev exec drush config:import` | Import config – very useful to test if config issues cause errors. | -| `ddev exec drush config:delete ` | Remove a config object (helps when orphaned config causes fatal errors).| + +| Command | Purpose | +| ------------------------------------------------- | -------------------------------------- | +| `ddev exec drush config:get ` | Show a single configuration value | +| `ddev exec drush config:set ` | Temporarily change a config value | +| `ddev exec drush config:export` | Export active config to sync directory | +| `ddev exec drush config:import` | Import config | +| `ddev exec drush config:delete ` | Remove a config object | #### Module/Theming Debugging -| Command | Purpose | -|----------------------------------------|-------------------------------------------------------------------------| -| `ddev exec drush pm:list --type=module --status=enabled` | List enabled modules. | -| `ddev exec drush pm:enable ` | Enable a module. | -| `ddev exec drush pm:uninstall ` | Fully uninstall a module (removes config and data). | -| `ddev exec drush pm:uninstall` without arguments → interactive mode is excellent for disabling suspected problematic modules quickly. | -| `ddev exec drush theme:debug` (Drupal 9.4+) | Lists all theme suggestions for a given route or render array. | + +| Command | Purpose | +| -------------------------------------------------------- | --------------------------- | +| `ddev exec drush pm:list --type=module --status=enabled` | List enabled modules | +| `ddev exec drush pm:enable ` | Enable a module | +| `ddev exec drush pm:uninstall ` | Fully uninstall a module | +| `ddev exec drush theme:debug` | Lists all theme suggestions | #### Database & Entity Debugging -| Command | Purpose | -|----------------------------------------------|-------------------------------------------------------------------------| -| `ddev exec drush sql:connect` | Outputs the CLI command to connect to the DB (useful for manual queries). | -| `ddev exec drush sql:query` | Run arbitrary SQL. | -| `ddev exec drush entity:info` | Show entity type definitions (useful when entity schema errors occur). | -| `ddev exec drush php` | Opens an interactive PHP shell with Drupal bootstrapped (like `ddev exec drush php:eval`). | -| `ddev exec drush php:eval "code"` | Execute arbitrary PHP code in Drupal context (great for quick debugging). Example: `ddev exec drush php:eval "dpm(\Drupal::state()->get('system.cron_last'));"` (with Devel) | - -#### Development & Error Reproduction -| Command | Purpose | -|----------------------------------------|-------------------------------------------------------------------------| -| `ddev exec drush php:eval "var_dump(function_exists('my_problematic_function'));"` | Quick test if a function exists or what it returns. | -| `ddev exec drush state:edit` / `ddev exec drush state:get/set/delete` | Inspect or override Drupal state values (often used by broken modules).| -| `ddev exec drush variable:get/set/delete` (D7 only) | Legacy equivalent of state commands. | -| `ddev exec drush twig:debug` | Turn Twig debugging on/off and verify template suggestions. | -| `ddev exec drush eval` | Same as above (alias of php:eval). | - -#### Performance & Query Debugging -| Command | Purpose | -|----------------------------------------|-------------------------------------------------------------------------| -| `ddev exec drush sql:query --db-prefix` | See queries with table prefixes expanded (helps reading raw SQL). | -| Enable Devel + `ddev exec drush kint` or `ddev exec drush dpm()` in code → instant output in terminal. | + +| Command | Purpose | +| --------------------------------- | ------------------------------------------------------- | +| `ddev exec drush sql:connect` | Outputs the CLI command to connect to the DB | +| `ddev exec drush sql:query` | Run arbitrary SQL | +| `ddev exec drush entity:info` | Show entity type definitions | +| `ddev exec drush php` | Opens an interactive PHP shell with Drupal bootstrapped | +| `ddev exec drush php:eval "code"` | Execute arbitrary PHP code in Drupal context | #### DDEV-Specific Debugging + ```bash # Enable Xdebug debugging # Add to .ddev/config.yaml: @@ -296,15 +1031,13 @@ ddev describe # Show environment details and status # Access PHP error logs ddev exec tail -f /var/log/apache2/error.log -# Debugging functions (use with devel module) -ddev exec php -r "kint(\Drupal::config('system.site')->get());" - # Database connection debugging ddev exec drush sql:connect # Test database connection ddev describe # Check environment status ``` ### Performance Profiling in DDEV + ```bash # Performance analysis ddev exec drush cr # Rebuild caches @@ -316,115 +1049,457 @@ ddev exec drush site:status # System status check ``` ### Version Control Workflow + - **Commit messages**: Format `[#123456] Brief descriptive title` - **Branch from**: `develop` branch for features - **Atomic commits**: One logical change per commit - **Before pushing**: Run linting and tests -## Testing & Quality Assurance +## Advanced Development Patterns -### PHPUnit Testing Framework -Aim for ≥ 80% code coverage. Drupal provides multiple test types: +### Events & EventSubscribers -```bash -# Run all tests with coverage -ddev exec vendor/bin/phpunit -v --coverage-html coverage/ +Prefer EventSubscribers over hooks for many use cases. They are more testable and follow Symfony conventions. -# Run specific test suites -ddev exec vendor/bin/phpunit --testsuite unit # Unit tests (fast) -ddev exec vendor/bin/phpunit --testsuite kernel # Kernel tests -ddev exec vendor/bin/phpunit --testsuite functional # Functional tests (slower) -ddev exec vendor/bin/phpunit --testsuite javascript # JavaScript tests +**EventSubscriber example**: -# Run specific tests -ddev exec vendor/bin/phpunit --filter MyModuleUnitTest -ddev exec vendor/bin/phpunit web/modules/custom/my_module/tests/src/Unit/ +```php +namespace Drupal\my_module\EventSubscriber; -# Run with custom configuration -SIMPLETEST_DB=sqlite://localhost/tmp.sqlite ddev exec vendor/bin/phpunit +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Event\RequestEvent; + +class MyEventSubscriber implements EventSubscriberInterface { + + public static function getSubscribedEvents(): array { + return [ + KernelEvents::REQUEST => ['onKernelRequest', 100], + KernelEvents::RESPONSE => ['onKernelResponse'], + ]; + } + + public function onKernelRequest(RequestEvent $event): void { + // Act on every request. + $request = $event->getRequest(); + // ... + } + + public function onKernelResponse(\Symfony\Component\HttpKernel\Event\ResponseEvent $event): void { + // Modify response. + } +} ``` -### Test Types and Examples - -#### Unit Tests (fastest) -- **Purpose**: Test individual classes and methods in isolation -- **Base class**: Extend `UnitTestCase` from `Drupal\Tests\UnitTestCase` -- **Speed**: Fastest test type, no Drupal bootstrap required -- **Isolation**: Test one piece of functionality at a time -- **Dependencies**: Mock external dependencies and services -- **Location**: Place in `tests/src/Unit/` directory -- **Use cases**: Service logic calculations, utility functions, data transformations -- **Best practices**: Keep tests small, focused, and deterministic - -#### Kernel Tests (with database) -- **Purpose**: Test Drupal interactions with minimal Drupal environment -- **Base class**: Extend `KernelTestBase` from `Drupal\KernelTests\KernelTestBase` -- **Environment**: Partial Drupal bootstrap with in-memory database -- **Modules**: Declare required modules in `$modules` static property -- **Database**: Uses SQLite in-memory database for speed -- **Location**: Place in `tests/src/Kernel/` directory -- **Use cases**: Entity CRUD operations, configuration validation, service registration -- **Setup**: Install modules and configuration in `setUp()` method - -#### Functional Tests (with browser) -- **Purpose**: Test complete user interactions through browser simulation -- **Base class**: Extend `BrowserTestBase` from `Drupal\Tests\BrowserTestBase` -- **Environment**: Full Drupal bootstrap with real browser -- **Speed**: Slowest test type, full page loads required -- **Theme**: Set `$defaultTheme` property (usually 'stark' or 'claro') -- **Location**: Place in `tests/src/Functional/` directory -- **Use cases**: Form submissions, page access, user permissions, JavaScript interactions -- **Browser simulation**: Uses Goutte/ChromeDriver for browser automation -- **Assertions**: Use `$this->assertSession()` for web assertions +Register in `my_module.services.yml`: -### Code Quality Tools in DDEV -```bash -# Static analysis (add to composer require) -ddev exec vendor/bin/phpstan analyse # PHPStan analysis -ddev exec vendor/bin/psalm # Psalm analysis +```yaml +my_module.event_subscriber: + class: Drupal\my_module\EventSubscriber\MyEventSubscriber + tags: + - { name: event_subscriber } +``` -# Security scanning -ddev exec vendor/bin/drupal-check # Check for deprecated code -ddev exec composer audit # Check for security advisories +**Drupal-specific events**: `HookEventDispatcher` module provides events for most Drupal hooks. Core events include entity events (`EntityBase::create()`, presave, etc.) and kernel events. -# Accessibility testing -ddev exec vendor/bin/phpunit --group accessibility # Accessibility tests +### Configuration Management + +**Config schema** (`config/schema/my_module.schema.yml`): + +```yaml +my_module.settings: + type: config_object + label: "My Module settings" + mapping: + api_key: + type: string + label: "API Key" + max_items: + type: integer + label: "Maximum items" + enabled_types: + type: sequence + label: "Enabled content types" + sequence: + type: string + label: "Content type" ``` -### JavaScript Testing +**Config install** (`config/install/my_module.settings.yml`): + +```yaml +api_key: "" +max_items: 50 +enabled_types: + - article +``` + +**Reading config**: + +```php +// In a service/controller (injected) +$value = $this->configFactory->get('my_module.settings')->get('api_key'); + +// In a .module file (less preferred) +$value = \Drupal::config('my_module.settings')->get('api_key'); +``` + +**Config workflow**: + ```bash -# Install JavaScript dependencies -ddev exec npm install +# Export all configuration +ddev exec drush config:export -# Run JavaScript tests -ddev exec npm run test # Jest tests -ddev exec npm run test:a11y # Accessibility tests +# Import configuration +ddev exec drush config:import + +# View a single config value +ddev exec drush config:get system.site + +# Edit config interactively +ddev exec drush config:edit my_module.settings ``` -### Before Submitting Code +- **`config/install/`**: Required config installed when module is enabled +- **`config/optional/`**: Config installed only if dependencies are met +- **Config override**: Use `$config['system.performance']['css']['preprocess'] = FALSE;` in `settings.php` for environment-specific overrides +- **Config split**: Use `config_split` module for per-environment configuration (dev/staging/prod) + +### Batch API for Long Operations + +```php +use Drupal\Core\Batch\BatchBuilder; + +function my_module_process_items(array $items): void { + $batch = (new BatchBuilder()) + ->setTitle(t('Processing items')) + ->setInitMessage(t('Initializing...')) + ->setProgressMessage(t('Processed @current out of @total.')) + ->setErrorMessage(t('An error occurred during processing.')) + ->setFile(\Drupal::service('extension.list.module')->getPath('my_module') . '/my_module.batch.inc') + ->setFinishCallback('my_module_batch_finished') + ->addOperation('my_module_batch_process', [$items]); + + batch_set($batch->toArray()); +} + +// In my_module.batch.inc +function my_module_batch_process(array $items, array &$context): void { + if (!isset($context['sandbox']['progress'])) { + $context['sandbox']['progress'] = 0; + $context['sandbox']['max'] = count($items); + $context['sandbox']['items'] = $items; + } + + $limit = 10; + $items_to_process = array_slice($context['sandbox']['items'], $context['sandbox']['progress'], $limit); + + foreach ($items_to_process as $item) { + // Process each item. + $context['sandbox']['progress']++; + } + + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; + $context['message'] = t('Processed @progress of @max items', [ + '@progress' => $context['sandbox']['progress'], + '@max' => $context['sandbox']['max'], + ]); +} + +function my_module_batch_finished(bool $success, array $results, array $operations): void { + $messenger = \Drupal::messenger(); + if ($success) { + $messenger->addStatus(t('Batch completed successfully.')); + } + else { + $messenger->addError(t('An error occurred during batch processing.')); + } +} +``` + +- **Purpose**: Process large datasets without PHP timeout issues +- **Use cases**: Data migration, bulk updates, file processing, API calls +- **Memory management**: Processes data in chunks to prevent memory exhaustion + +### Queue API for Background Processing + +```php +namespace Drupal\my_module\Plugin\QueueWorker; + +use Drupal\Core\Queue\QueueWorkerBase; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Processes my module items. + * + * @QueueWorker( + * id = "my_module_processor", + * title = @Translation("My Module Processor"), + * cron = {"time" = 60} + * ) + */ +class MyQueueWorker extends QueueWorkerBase implements ContainerFactoryPluginInterface { + + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { + return new static($configuration, $plugin_id, $plugin_definition); + } + + public function processItem($data): void { + // Process the queue item. + if (!isset($data['type'])) { + throw new \InvalidArgumentException('Missing type in queue item.'); + } + // Do work... + } +} +``` + +**Adding items to the queue**: + +```php +\Drupal::queue('my_module_processor')->createItem(['type' => 'cleanup', 'node_id' => 123]); +``` + +- **Cron integration**: `cron = {"time" = 60}` processes items during cron for up to 60 seconds +- **Reliability**: Failed items are released back to the queue automatically +- **Logging**: Always log queue processing outcomes + +### AJAX Forms + +- **Trigger elements**: Add `#ajax` property to form elements (select, checkbox, button) +- **Callback method**: Reference callback method using `::methodName` syntax +- **Wrapper element**: Specify target element ID for AJAX response replacement +- **Response format**: Return form element or render array from callback +- **Event types**: Use 'change', 'click', 'blur' events as needed +- **Progress indicator**: Automatically shows loading indicator during AJAX requests +- **Error handling**: Implement try-catch blocks in AJAX callbacks +- **Form state**: Use `$form_state->getTriggeringElement()` to identify trigger +- **Multiple triggers**: Can have multiple AJAX elements in same form + +### Render API Deep Dive + +```php +// Full render array with all common properties +$build = [ + '#type' => 'container', + '#attributes' => ['class' => ['my-wrapper']], + 'heading' => [ + '#type' => 'html_tag', + '#tag' => 'h2', + '#value' => $this->t('My Heading'), + ], + 'content' => [ + '#theme' => 'item_list', + '#items' => $items, + '#empty' => $this->t('No items found.'), + ], + '#cache' => [ + 'keys' => ['my_module', 'list', $categoryId], + 'tags' => ['node_list', 'taxonomy_term:' . $categoryId], + 'contexts' => ['user.roles', 'languages:language_content'], + 'max-age' => 3600, + ], + '#attached' => [ + 'library' => ['my_module/my_module.styles'], + 'drupalSettings' => [ + 'my_module' => ['endpoint' => '/api/items'], + ], + ], + '#weight' => 10, +]; +``` + +- **`#pre_render` / `#post_render`**: Callbacks to modify render arrays before/after rendering +- **`#lazy_builder`**: Defers rendering of expensive content +- **`#create_placeholder`**: Generates a placeholder for BigPipe-style loading +- **`#attached`**: Attach CSS/JS libraries, settings, HTML head links, and HTTP headers + +### Migration API + +```php +// In migrations/my_migration.yml — source plugin +source: + plugin: csv + path: /path/to/data.csv + header_row_count: 1 + keys: + - id + column_names: + - + id: [id, 'Unique ID'] + - + title: [title, 'Title'] + +# Process plugin +process: + title: title + body/value: body + body/format: + plugin: default_value + default_value: basic_html + type: + plugin: default_value + default_value: article + uid: + plugin: default_value + default_value: 1 + +# Destination plugin +destination: + plugin: entity:node + default_bundle: article +``` + +**Custom process plugin**: + +```php +namespace Drupal\my_module\Plugin\migrate\process; + +use Drupal\migrate\ProcessPluginBase; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Row; + +/** + * Custom process plugin. + * + * @MigrateProcessPlugin( + * id = "my_custom_process" + * ) + */ +class MyCustomProcess extends ProcessPluginBase { + + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property): mixed { + // Transform the value during migration. + return strtoupper(trim($value)); + } +} +``` + +### Composer Management + ```bash -# Quality checklist -ddev exec vendor/bin/phpcs --standard=Drupal . # Code style -ddev exec vendor/bin/phpunit # Run tests -ddev exec drush cr # Clear caches -ddev exec drush updatedb # Run updates +# Add a module +ddev composer require drupal/admin_toolbar + +# Add a module with a patch +ddev composer require drupal/some_module +# Then add patch to composer.json extras: +# "patches": { +# "drupal/some_module": { +# "Fix description": "https://www.drupal.org/files/issues/2024-01-01/issue-12345-1.patch" +# } +# } + +# Update Drupal core +ddev composer update drupal/core --with-all-dependencies + +# Run post-install steps +ddev exec drush updatedb +ddev exec drush config:import +ddev exec drush cr +``` + +**composer.json best practices**: + +- Use `drupal/core-recommended` for production, `drupal/core-dev` for development +- Pin major versions: `"drupal/core-recommended": "^11"` +- Use `composer-patches` plugin for community patches +- Commit `composer.lock` to version control +- Use `drupal.org` composer endpoint: `composer config repositories.drupal composer https://packages.drupal.org/8` + +### JavaScript & Frontend + +**Drupal behaviors** (not jQuery document.ready): + +```javascript +// js/my-module.js +(function (Drupal, drupalSettings) { + "use strict"; + + Drupal.behaviors.myModuleBehavior = { + attach: function (context, settings) { + // Run on every page load and AJAX response. + const elements = context.querySelectorAll(".my-element"); + elements.forEach(function (element) { + element.addEventListener("click", handleClick); + }); + }, + detach: function (context, settings, trigger) { + // Clean up when content is removed (AJAX, etc.). + const elements = context.querySelectorAll(".my-element"); + elements.forEach(function (element) { + element.removeEventListener("click", handleClick); + }); + }, + }; + + function handleClick(event) { + // Handle click. + } +})(Drupal, drupalSettings); +``` + +**Library definition** (`my_module.libraries.yml`): + +```yaml +my_module.styles: + version: VERSION + css: + component: + css/my-module.css: {} + js: + js/my-module.js: {} + dependencies: + - core/drupal + - core/drupalSettings +``` + +**Attaching libraries**: + +```php +// In render array +$build['#attached']['library'][] = 'my_module/my_module.styles'; + +// In twig +{{ + attach_library('my_module/my_module.styles') +}} +``` + +### Content Moderation & Workflows + +```php +// Workflows are typically configured via UI, but modules can interact: +use Drupal\workflows\Entity\Workflow; + +// Load a workflow +$workflow = Workflow::load('editorial'); + +// Check moderation state of a node +if ($node->hasField('moderation_state')) { + $state = $node->get('moderation_state')->value; +} + +// Transition a node to a new state +$node->set('moderation_state', 'published'); +$node->save(); ``` ## DDEV-Specific Troubleshooting ### Common DDEV Issues + ```bash # DDEV won't start ddev poweroff && ddev start -# Port conflicts -# Edit .ddev/config.yaml to change ports -router_http_port: "8080" -router_https_port: "8443" +# Port conflicts — edit .ddev/config.yaml to change ports +# router_http_port: "8080" +# router_https_port: "8443" -# Memory issues -# Increase PHP memory in .ddev/php/php.ini -memory_limit = 512M +# Memory issues — increase PHP memory in .ddev/php/php.ini +# memory_limit = 512M # Composer memory issues ddev exec php -d memory_limit=-1 /usr/local/bin/composer install @@ -435,6 +1510,7 @@ ddev exec drush sql:connect # Test database connection ``` ### Performance Issues in DDEV + ```bash # Identify slow queries ddev exec drush sql:query "SELECT * FROM watchdog WHERE type = 'php' ORDER BY wid DESC LIMIT 10" @@ -447,6 +1523,7 @@ ddev exec drush pm:enable memcache redis -y ``` ### Module/Theme Development Issues in DDEV + ```bash ddev exec drush cr @@ -462,70 +1539,38 @@ ddev exec drush watchdog:show --type=cron ``` ### Testing Issues in DDEV + ```bash -# PHPUnit configuration problems -# Ensure phpunit.xml.dist exists and is configured +# PHPUnit configuration — ensure phpunit.xml.dist exists and is configured cp web/core/phpunit.xml.dist phpunit.xml -# Database setup for testing -# Edit phpunit.xml for SIMPLETEST_DB and SIMPLETEST_BASE_URL -SIMPLETEST_DB=mysql://db:db@db/db_test -SIMPLETEST_BASE_URL=http://my-drupal-project.ddev.site +# Database setup for testing — edit phpunit.xml for SIMPLETEST_DB and SIMPLETEST_BASE_URL +# SIMPLETEST_DB=mysql://db:db@db/db_test +# SIMPLETEST_BASE_URL=http://my-drupal-project.ddev.site -# Browser tests failing -# Install Selenium or ChromeDriver +# Browser tests failing — install Selenium or ChromeDriver # Ensure test environment variables are set ``` -## Advanced Development Patterns - -### Batch API for Long Operations -- **Purpose**: Process large datasets without PHP timeout issues -- **Use cases**: Data migration, bulk updates, file processing, API calls -- **Batch structure**: Create associative array with title, operations, and finished callback -- **Operations**: Array of callable methods and their arguments -- **Progress tracking**: Automatically shows progress bar to users -- **Error handling**: Implement proper exception handling in batch operations -- **User experience**: Provides real-time feedback during long operations -- **Memory management**: Processes data in chunks to prevent memory exhaustion - -### Queue API for Background Processing -- **Purpose**: Process tasks in the background without blocking user interaction -- **Queue creation**: Use `\Drupal::queue('queue_name')` to get queue instance -- **Item addition**: Use `createItem()` to add tasks to the queue -- **Processing**: Claim items with `claimItem()` and delete with `deleteItem()` -- **Cron integration**: Process queue items during cron runs for regular background tasks -- **Reliability**: Failed items can be released back to the queue -- **Worker plugins**: Create QueueWorker plugins for structured queue processing -- **Logging**: Implement proper logging for queue processing monitoring -- **Performance**: Process multiple items per cron run for efficiency - -### AJAX Forms -- **Trigger elements**: Add `#ajax` property to form elements (select, checkbox, button) -- **Callback method**: Reference callback method using `::methodName` syntax -- **Wrapper element**: Specify target element ID for AJAX response replacement -- **Response format**: Return form element or render array from callback -- **Event types**: Use 'change', 'click', 'blur' events as needed -- **Progress indicator**: Automatically shows loading indicator during AJAX requests -- **Error handling**: Implement try-catch blocks in AJAX callbacks -- **Form state**: Use `$form_state->getTriggeringElement()` to identify trigger -- **Multiple triggers**: Can have multiple AJAX elements in same form -- **Dynamic forms**: Update form options, show/hide fields based on user input - ## Additional Resources ### DDEV Documentation + - **DDEV Official Docs**: https://ddev.readthedocs.io - **DDEV Quick Start**: https://ddev.readthedocs.io/en/stable/users/quickstart/ - **DDEV Drupal Guide**: https://ddev.readthedocs.io/en/stable/users/topics/drupal/ ### Drupal Documentation + - **Drupal API**: https://api.drupal.org - **Developer Guide**: https://www.drupal.org/docs/develop - **Coding Standards**: https://www.drupal.org/docs/develop/standards - **Security Best Practices**: https://www.drupal.org/docs/develop/security +- **Configuration Management**: https://www.drupal.org/docs/administering-a-drupal-site/configuration-management +- **Migration API**: https://www.drupal.org/docs/8/api/migrate-api ### Community Resources + - **DrupalAtYourFingertips**: https://www.drupalatyourfingertips.com - **Drupal Answers**: https://drupal.stackexchange.com - **Drupal.org**: https://www.drupal.org diff --git a/Lagoon/AGENTS.md b/Lagoon/AGENTS.md new file mode 100644 index 0000000..467a559 --- /dev/null +++ b/Lagoon/AGENTS.md @@ -0,0 +1,1425 @@ +# AGENTS.md: AI Agent Guide for Drupal Development on Lagoon + +**AI Agent Instructions**: This guide provides comprehensive instructions for AI coding agents working on Drupal projects deployed on amazee.io Lagoon (Kubernetes-based hosting). Follow these guidelines for consistent, high-quality contributions. Human contributors should use README.md instead. + +## Table of Contents + +- [Project Overview](#project-overview) +- [Lagoon Quick Setup](#lagoon-quick-setup) +- [Module Scaffolding Template](#module-scaffolding-template) +- [Code Style and Standards](#code-style-and-standards) +- [Drupal Development Patterns](#drupal-development-patterns) +- [Security & Performance Guidelines](#security--performance-guidelines) +- [Anti-Patterns — Never Do This](#anti-patterns--never-do-this) +- [Testing & Quality Assurance](#testing--quality-assurance) +- [Lagoon Development Workflow](#lagoon-development-workflow) +- [Advanced Development Patterns](#advanced-development-patterns) +- [Additional Topics](#additional-topics) +- [Troubleshooting](#troubleshooting) +- [Additional Resources](#additional-resources) + +## Project Overview + +- **Core Technology**: Drupal 10.x / 11.x (verify via `composer show drupal/core`) +- **Hosting Platform**: amazee.io Lagoon (Kubernetes-based) +- **Local Development**: DDEV or Docker Compose (Lagoon-compatible) +- **Key Components**: Custom modules, themes, configuration management, Composer dependencies +- **Environment**: PHP 8.3+, MariaDB, Nginx, Varnish (managed by Lagoon) +- **Development Tools**: Composer, Drush 13+, Git, Lagoon CLI, lagoon-sync +- **Important**: Use Drush aliases for remote operations. Local commands run directly or via DDEV. + +## Lagoon Quick Setup + +### Prerequisites + +```bash +# Install Lagoon CLI (macOS) +brew tap uselagoon/lagoon-cli +brew install lagoon + +# Or download from https://github.com/uselagoon/lagoon-cli/releases +# Verify installation +lagoon --version + +# Install lagoon-sync for database/file synchronization +brew install uselagoon/lagoon-sync/lagoon-sync + +# Configure Lagoon CLI connection +lagoon config add \ + --graphql YOUR-API-URL/graphql \ + --ui YOUR-UI-URL \ + --hostname YOUR.DOMAIN +``` + +### Lagoon Project Files + +Lagoon requires these files in the repository root: + +**`.lagoon.yml`** — Lagoon configuration: + +```yaml +docker-compose-yaml: docker-compose.yml + +# Environment variables +environment_variables: + git_sha: "true" + +# Environments that auto-deploy +environments: + main: + routes: + - nginx: + - example.com + - www.example.com + cronjobs: + - name: drush cron + schedule: "*/15 * * * *" + command: drush cron + service: nginx + +# Post-rollout tasks +tasks: + post-rollout: + - run: + name: drush updb + command: drush updatedb --no-cache-clear + service: nginx + shell: bash + - run: + name: drush cim + command: drush config:import --yes + service: nginx + shell: bash + - run: + name: drush cr + command: drush cache:rebuild + service: nginx + shell: bash +``` + +**`docker-compose.yml`** (Lagoon-flavored): + +```yaml +# Must use the Lagoon-compatible docker-compose format +# See: https://docs.lagoon.sh/lagoon/using-lagoon-the-basics/docker-compose-yml/ +``` + +### Essential Lagoon Commands + +```bash +# Deployment +lagoon deploy branch --project --branch # Deploy a branch +lagoon deploy promote --project --source --destination # Promote to production + +# Environment management +lagoon list environments --project # List environments +lagoon get environment --project --environment # Get env details +lagoon delete environment --project --environment # Delete env + +# Logs & debugging +lagoon logs --project --environment # View environment logs +lagoon ssh --project --environment # SSH into pod + +# Variables +lagoon list variables --project --environment +lagoon add variable --project --environment --name NAME --value VALUE +``` + +### Database & File Synchronization + +```bash +# Sync database from production to local +lagoon-sync sync mariadb -p -e main -t local + +# Sync database from staging to local +lagoon-sync sync mariadb -p -e staging -t local + +# Sync files from production to local +lagoon-sync sync files -p -e main -t local + +# Using Drush aliases (alternative) +drush sql:sync @lagoon.main @self +drush rsync @lagoon.main:%files @self:%files +``` + +### Environment Variables + +Lagoon automatically injects these variables: + +| Variable | Description | +| ------------------------- | ----------------------------------------- | +| `LAGOON_PROJECT` | Project name | +| `LAGOON_ENVIRONMENT` | Environment name (branch) | +| `LAGOON_ENVIRONMENT_TYPE` | `production`, `staging`, or `development` | +| `LAGOON_GIT_BRANCH` | Git branch name | +| `LAGOON_GIT_SHA` | Full Git commit SHA | +| `LAGOON_ROUTE` | Primary route/URL of the environment | +| `LAGOON_ROUTES` | Comma-separated list of all routes | + +Use these in `settings.php` for environment-aware configuration: + +```php +// settings.php — Lagoon environment detection +$lagoon_env_type = getenv('LAGOON_ENVIRONMENT_TYPE') ?: 'local'; +$is_production = $lagoon_env_type === 'production'; + +if ($is_production) { + $config['system.performance']['css']['preprocess'] = TRUE; + $config['system.performance']['js']['preprocess'] = TRUE; +} +else { + // Development settings. + $config['system.performance']['css']['preprocess'] = FALSE; + $config['system.performance']['js']['preprocess'] = FALSE; + $settings['cache']['bins']['render'] = 'cache.backend.null'; +} +``` + +### Drush Aliases + +Lagoon provides Drush aliases automatically. Use them for remote operations: + +```bash +# List available aliases +drush site:alias + +# Run Drush commands on remote environments +drush @lagoon.main status +drush @lagoon.staging config:export +drush @lagoon.main cache:rebuild + +# Sync between environments +drush sql:sync @lagoon.main @lagoon.staging +drush rsync @lagoon.main:%files @lagoon.staging:%files +``` + +## Module Scaffolding Template + +When creating a new custom module, follow this structure: + +``` +web/modules/custom/my_module/ +├── my_module.info.yml # Module metadata (required) +├── my_module.module # Hook implementations +├── my_module.routing.yml # Route definitions +├── my_module.services.yml # Service definitions +├── my_module.permissions.yml # Permission definitions +├── my_module.links.menu.yml # Menu links +├── my_module.links.action.yml # Action links +├── my_module.links.task.yml # Task (tab) links +├── my_module.libraries.yml # CSS/JS libraries +├── composer.json # PSR-4 autoloading +├── src/ +│ ├── Controller/ +│ │ └── MyController.php +│ ├── Form/ +│ │ ├── SettingsForm.php # ConfigFormBase +│ │ └── CustomForm.php # FormBase +│ ├── Plugin/ +│ │ ├── Block/ +│ │ │ └── MyBlock.php +│ │ ├── Field/ +│ │ │ ├── FieldFormatter/ +│ │ │ │ └── MyFormatter.php +│ │ │ ├── FieldWidget/ +│ │ │ │ └── MyWidget.php +│ │ │ └── FieldType/ +│ │ │ └── MyFieldItem.php +│ │ └── QueueWorker/ +│ │ └── MyQueueWorker.php +│ ├── EventSubscriber/ +│ │ └── MyEventSubscriber.php +│ ├── Access/ +│ │ └── MyAccessChecker.php +│ ├── Entity/ +│ │ └── MyEntity.php +│ └── Service/ +│ └── MyService.php +├── config/ +│ ├── install/ # Config installed with module +│ │ └── my_module.settings.yml +│ ├── optional/ # Config installed only if dependencies met +│ │ └── field.field.node.article.field_my_field.yml +│ └── schema/ # Config schema for typed data +│ └── my_module.schema.yml +├── templates/ +│ └── my-module-template.html.twig +├── css/ +│ └── my-module.css +├── js/ +│ └── my-module.js +└── tests/ + └── src/ + ├── Unit/ + │ └── MyServiceTest.php + ├── Kernel/ + │ └── MyModuleKernelTest.php + └── Functional/ + └── MyModuleFunctionalTest.php +``` + +### Minimal Module Files + +**my_module.info.yml**: + +```yaml +name: "My Module" +type: module +description: "Custom module description." +core_version_requirement: ^10 || ^11 +package: Custom +dependencies: + - drupal:node + - drupal:user +``` + +**composer.json** (for PSR-4 autoloading in tests): + +```json +{ + "name": "drupal/my_module", + "type": "drupal-custom-module", + "description": "Custom module description.", + "autoload": { + "psr-4": { + "Drupal\\my_module\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Drupal\\Tests\\my_module\\": "tests/src/" + } + } +} +``` + +## Code Style and Standards + +Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and PHPCS for enforcement. + +- **PHP**: + - Indentation: 2 spaces (no tabs) + - Line length: ≤ 80 characters + - Naming: CamelCase classes/methods, snake_case variables/functions + - Always use braces; prefer early returns + - Full PHPDoc blocks with `@param`, `@return`, `@throws` + +- **YAML**: 2-space indentation, lowercase keys +- **Twig**: `{{ }}` for output, `{% %}` for logic; always escape with `|e` + +- **Linting** (run locally or via DDEV): + + ```bash + vendor/bin/phpcs --standard=Drupal --extensions=php,inc,module,install,info,yml src/ + vendor/bin/phpcs --standard=DrupalPractice --extensions=php,inc,module,install,info,yml src/ + vendor/bin/phpcs --standard=Drupal --fix src/ + ``` + +**Reject any code that fails Drupal Coder sniffs.** + +## Drupal Development Patterns + +### Services & Dependency Injection + +**Create services** in `modulename.services.yml`: + +```yaml +# my_module.services.yml +services: + my_module.my_service: + class: Drupal\my_module\Service\MyService + arguments: ["@entity_type.manager", "@logger.factory", "@config.factory"] + tags: + - { name: backend_overridable } +``` + +**Use dependency injection** in controllers, forms, and plugins: + +```php +namespace Drupal\my_module\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +class MyController extends ControllerBase { + + public function __construct( + protected EntityTypeManagerInterface $entityTypeManager, + ) {} + + public static function create(ContainerInterface $container): static { + return new static( + $container->get('entity_type.manager'), + ); + } + + public function content(): array { + $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple(); + // ... + return $build; + } +} +``` + +- **Core services** like `@current_user`, `@entity_type.manager`, `@database`, `@config.factory`, `@logger.factory` are available +- **Best practice**: Avoid static `\Drupal::` calls in favor of dependency injection +- **Service discovery**: Use `drush php:eval "print_r(\Drupal::getContainer()->getServiceIds());"` to see available services +- **Location**: Place service classes in `src/` directory with proper namespace + +### Entity API & Queries + +**Loading entities**: + +```php +// Single entity +$node = \Drupal::entityTypeManager()->getStorage('node')->load(123); + +// Multiple entities +$nodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple([1, 2, 3]); + +// Load by properties +$nodes = \Drupal::entityTypeManager()->getStorage('node')->loadByProperties([ + 'type' => 'article', + 'status' => 1, +]); +``` + +**Entity queries** (always prefer over raw SQL): + +```php +use Drupal\Core\Entity\Query\QueryInterface; + +// Modern entity query with injected service +$ids = $this->entityTypeManager->getStorage('node')->getQuery() + ->condition('type', 'article') + ->condition('status', 1) + ->condition('field_category', $categoryId) + ->sort('created', 'DESC') + ->range(0, 10) + ->accessCheck(TRUE) // ALWAYS set explicitly in Drupal 10.2+ + ->execute(); + +$nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($ids); +``` + +**Creating entities**: + +```php +$node = \Drupal::entityTypeManager()->getStorage('node')->create([ + 'type' => 'article', + 'title' => 'My Article', + 'body' => [ + 'value' => 'Content here', + 'format' => 'full_html', + ], + 'status' => 1, + 'uid' => 1, +]); +$node->save(); +``` + +**Field access**: Use entity field API instead of direct property access: + +```php +// Correct +$node->get('field_my_field')->value; +$node->get('field_my_field')->entity; // For entity reference fields + +// Avoid +$node->field_my_field->value; // Magic __get — works but less explicit +``` + +### Plugin System + +**Block plugin example**: + +```php +namespace Drupal\my_module\Plugin\Block; + +use Drupal\Core\Block\BlockBase; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides a 'My Custom Block' block. + * + * @Block( + * id = "my_custom_block", + * admin_label = @Translation("My Custom Block"), + * category = @Translation("Custom"), + * context_definitions = { + * "node" = @ContextDefinition("entity:node", label = @Translation("Node")) + * } + * ) + */ +class MyCustomBlock extends BlockBase implements ContainerFactoryPluginInterface { + + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { + return new static($configuration, $plugin_id, $plugin_definition); + } + + public function build(): array { + return [ + '#markup' => $this->t('Hello from my custom block!'), + '#cache' => [ + 'tags' => ['node_list'], + 'contexts' => ['user.roles'], + ], + ]; + } +} +``` + +- **Plugin types**: Blocks, field formatters, field widgets, field types, menu links, QueueWorker, Condition, Action, and more +- **Plugin discovery**: Use annotation-based discovery (as shown above) or YAML discovery +- **Plugin base classes**: Extend appropriate base classes (`BlockBase`, `FormatterBase`, `WidgetBase`, etc.) +- **Plugin placement**: Place plugins in `src/Plugin/Type/` directory structure +- **Derivative plugins**: Use `DeriverBase` for creating multiple plugins from one definition + +### Hooks + +**Implement hooks** in `modulename.module` file: + +```php +/** + * Implements hook_form_alter(). + */ +function my_module_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id): void { + if ($form_id === 'node_article_form') { + $form['title']['#title'] = t('Article Title'); + $form['actions']['submit']['#value'] = t('Publish Article'); + } +} + +/** + * Implements hook_theme(). + */ +function my_module_theme($existing, $type, $theme, $path): array { + return [ + 'my_template' => [ + 'variables' => [ + 'title' => '', + 'items' => [], + ], + 'template' => 'my-template', + ], + ]; +} + +/** + * Implements hook_entity_presave(). + */ +function my_module_entity_presave(\Drupal\Core\Entity\EntityInterface $entity): void { + if ($entity->getEntityTypeId() === 'node' && $entity->bundle() === 'article') { + // Auto-set a field before saving. + $entity->set('field_last_updated', \Drupal::time()->getRequestTime()); + } +} + +/** + * Implements hook_cron(). + */ +function my_module_cron(): void { + // Process items during cron runs. + \Drupal::queue('my_module_processor')->createItem(['type' => 'cleanup']); +} +``` + +- **Hook naming**: Follow pattern `hook_modulename_action()` for custom hooks +- **Hook parameters**: Use type hints and proper parameter documentation +- **Core hooks**: Common hooks include `hook_form_alter()`, `hook_theme()`, `hook_entity_presave()`, `hook_cron()`, `hook_menu_links_discovered_alter()` +- **Hook order**: Hooks fire in module weight order (lowest first) +- **Best practice**: Keep hook implementations focused and delegate complex logic to services + +### Forms API + +**Simple form**: + +```php +namespace Drupal\my_module\Form; + +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; + +class CustomForm extends FormBase { + + public function getFormId(): string { + return 'my_module_custom_form'; + } + + public function buildForm(array $form, FormStateInterface $form_state): array { + $form['email'] = [ + '#type' => 'email', + '#title' => $this->t('Email Address'), + '#required' => TRUE, + ]; + + $form['category'] = [ + '#type' => 'select', + '#title' => $this->t('Category'), + '#options' => [ + 'news' => $this->t('News'), + 'events' => $this->t('Events'), + 'blog' => $this->t('Blog'), + ], + '#ajax' => [ + 'callback' => '::categoryChanged', + 'wrapper' => 'subcategory-wrapper', + 'event' => 'change', + ], + ]; + + $form['subcategory'] = [ + '#type' => 'container', + '#attributes' => ['id' => 'subcategory-wrapper'], + 'value' => [ + '#type' => 'textfield', + '#title' => $this->t('Subcategory'), + ], + ]; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + public function categoryChanged(array &$form, FormStateInterface $form_state): array { + return $form['subcategory']; + } + + public function validateForm(array &$form, FormStateInterface $form_state): void { + $email = $form_state->getValue('email'); + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + $form_state->setErrorByName('email', $this->t('Please enter a valid email address.')); + } + } + + public function submitForm(array &$form, FormStateInterface $form_state): void { + $this->messenger()->addStatus($this->t('Form submitted successfully.')); + $form_state->setRedirect(''); + } +} +``` + +**Configuration form**: + +```php +namespace Drupal\my_module\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; + +class SettingsForm extends ConfigFormBase { + + public function getFormId(): string { + return 'my_module_settings'; + } + + protected function getEditableConfigNames(): array { + return ['my_module.settings']; + } + + public function buildForm(array $form, FormStateInterface $form_state): array { + $config = $this->config('my_module.settings'); + + $form['api_key'] = [ + '#type' => 'textfield', + '#title' => $this->t('API Key'), + '#default_value' => $config->get('api_key'), + '#required' => TRUE, + ]; + + $form['max_items'] = [ + '#type' => 'number', + '#title' => $this->t('Maximum Items'), + '#default_value' => $config->get('max_items') ?? 50, + '#min' => 1, + '#max' => 500, + ]; + + return parent::buildForm($form, $form_state); + } + + public function submitForm(array &$form, FormStateInterface $form_state): void { + $this->config('my_module.settings') + ->set('api_key', $form_state->getValue('api_key')) + ->set('max_items', $form_state->getValue('max_items')) + ->save(); + + parent::submitForm($form, $form_state); + } +} +``` + +### Routes & Controllers + +**Route definition** (`my_module.routing.yml`): + +```yaml +my_module.content: + path: "/my-module/{node}" + defaults: + _controller: '\Drupal\my_module\Controller\MyController::content' + _title: "My Module Page" + requirements: + _permission: "access content" + node: \d+ + +my_module.settings: + path: "/admin/config/my-module/settings" + defaults: + _form: '\Drupal\my_module\Form\SettingsForm' + _title: "My Module Settings" + requirements: + _permission: "administer site configuration" + +my_module.custom_access: + path: "/my-module/custom/{node}" + defaults: + _controller: '\Drupal\my_module\Controller\MyController::customPage' + _title_callback: '\Drupal\my_module\Controller\MyController::pageTitle' + requirements: + _custom_access: '\Drupal\my_module\Access\MyAccessChecker::access' +``` + +**Controller**: + +```php +namespace Drupal\my_module\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\node\NodeInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +class MyController extends ControllerBase { + + public function __construct( + protected EntityTypeManagerInterface $entityTypeManager, + ) {} + + public static function create(ContainerInterface $container): static { + return new static( + $container->get('entity_type.manager'), + ); + } + + public function content(NodeInterface $node): array { + return [ + '#theme' => 'my_template', + '#title' => $node->label(), + '#items' => $this->getItems($node), + '#cache' => [ + 'tags' => ['node:' . $node->id()], + 'contexts' => ['user.permissions'], + ], + '#attached' => [ + 'library' => ['my_module/my_module.styles'], + ], + ]; + } + + public function pageTitle(NodeInterface $node): string { + return $this->t('Page: @title', ['@title' => $node->label()]); + } +} +``` + +**Custom access checker**: + +```php +namespace Drupal\my_module\Access; + +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Routing\Access\AccessInterface; +use Drupal\node\NodeInterface; + +class MyAccessChecker implements AccessInterface { + + public function access(NodeInterface $node): AccessResult { + return AccessResult::allowedIf($node->isPublished()) + ->addCacheTags(['node:' . $node->id()]) + ->cachePerUser(); + } +} +``` + +Register in `my_module.services.yml`: + +```yaml +my_module.access_checker: + class: Drupal\my_module\Access\MyAccessChecker + tags: + - { name: access_check } +``` + +## Security & Performance Guidelines + +### Security Requirements + +- **Always sanitize user input**: Use `#plain_text` for untrusted content +- **CSRF protection**: Include `#token` for forms with side effects +- **Permissions**: Implement proper access checks and route requirements +- **SQL Injection**: Use Entity Query or proper parameter binding +- **XSS Prevention**: Always use `|e` filter in Twig, `#markup` for trusted HTML only +- **File uploads**: Validate file types and sizes; use Drupal's file API +- **Database credentials**: Never commit credentials — Lagoon injects them via environment variables +- **Render arrays**: Never use `#markup` with unsanitized user input; use `#plain_text` or `check_plain()` + +### Performance Best Practices + +- **Render caching**: Always add `#cache` array to render arrays with appropriate `tags` and `contexts` +- **Cache tags**: Use entity-based tags like `['node:123']` or list-based tags like `['node_list']` +- **Cache contexts**: Apply user-specific contexts like `['user.roles']` for personalized content +- **Lazy loading**: Use `#lazy_builder` for expensive operations that can be loaded separately +- **Placeholder strategy**: Set `#create_placeholder` => TRUE for lazy builders to improve initial page load +- **Cache max-age**: Set appropriate `max-age` values based on content freshness requirements +- **Varnish**: Lagoon provides Varnish by default — ensure proper cache headers and invalidation +- **Redis**: Lagoon supports Redis — configure in `settings.php` for distributed caching +- **Database queries**: Use entity queries instead of raw SQL for better caching and security +- **Entity loading**: Load multiple entities at once with `loadMultiple()` instead of individual loads + +**Render array with caching**: + +```php +$build = [ + '#theme' => 'item_list', + '#items' => $items, + '#cache' => [ + 'keys' => ['my_module:item_list:' . $categoryId], + 'tags' => ['node_list', 'taxonomy_term:' . $categoryId], + 'contexts' => ['user.roles'], + 'max-age' => 3600, + ], +]; +``` + +**Redis configuration for Lagoon** (in `settings.php`): + +```php +// Redis configuration for Lagoon +if (getenv('LAGOON')) { + $settings['redis.connection']['interface'] = 'PhpRedis'; + $settings['redis.connection']['host'] = getenv('REDIS_HOST') ?: 'redis'; + $settings['redis.connection']['port'] = getenv('REDIS_SERVICE_PORT') ?: 6379; + $settings['cache']['default'] = 'cache.backend.redis'; + $settings['container_yamls'][] = DRUPAL_ROOT . '/sites/redis.services.yml'; +} +``` + +### Caching Strategies + +- **Varnish (Lagoon default)**: Full-page caching for anonymous users with automatic purge +- **Redis**: Persistent object cache — configure via `settings.php` +- **Render cache**: Cache complex markup with proper tags/contexts +- **Dynamic page cache**: Automatically handles cacheability for anonymous users +- **Entity cache**: Core entity caching is automatic — invalidate with cache tags + +## Anti-Patterns — Never Do This + +These are common mistakes that an AI agent must avoid: + +1. **Never use `\Drupal::` static calls in services, controllers, or plugins** — Use dependency injection instead. The only acceptable use is in `hook_` functions in `.module` files (and even there, consider delegating to a service). + +2. **Never query the database directly when Entity Query suffices** — Use `\Drupal::entityQuery()` or injected `$this->entityTypeManager->getStorage()->getQuery()`. + +3. **Never use `|raw` in Twig** — Use `|e` (or rely on auto-escaping). If you need raw HTML, use `#type => 'processed_text'` or `check_markup()`. + +4. **Never create monolithic `hook_form_alter()` functions** — If the alter logic is complex, delegate to a service. Break large hooks into focused helper methods. + +5. **Never store configuration in state that belongs in config** — State (`\Drupal::state()`) is for ephemeral/transient data (last cron run, temporary flags). Config (`\Drupal::configFactory()`) is for structured, exportable settings. + +6. **Never use `#markup` with unsanitized user input** — Always use `#plain_text` for untrusted content or `check_plain()` / `Html::escape()` for escaping. + +7. **Never skip `accessCheck(TRUE)` on entity queries** — Drupal 10.2+ requires explicit access checking on entity queries. Omitting it throws deprecation warnings and will be required in Drupal 12. + +8. **Never hardcode entity IDs, user IDs, or paths** — Use configuration, route names, and dynamic lookups instead. + +9. **Never use `hook_views_data()` without proper table aliases** — Always prefix table columns clearly to avoid SQL ambiguity. + +10. **Never ignore cacheability metadata** — Every render array that depends on data must specify `#cache` tags, contexts, and max-age. Missing cache metadata causes stale content or unnecessary cache invalidation. + +11. **Never commit `settings.php` with database credentials** — On Lagoon, credentials are injected via environment variables automatically. + +12. **Never use `node_load()` or other deprecated procedural functions** — Use the entity type manager: `\Drupal::entityTypeManager()->getStorage('node')->load()`. + +13. **Never use global variables like `$_GET`, `$_POST`, `$_SERVER`** — Use Symfony's `Request` object via dependency injection. + +14. **Never put business logic in `.module` files** — Delegate to services. The `.module` file should be thin: route hooks, theme hooks, and thin wrappers that call services. + +## Testing & Quality Assurance + +### PHPUnit Testing Framework + +Aim for ≥ 80% code coverage. Drupal provides multiple test types: + +```bash +# Run all tests with coverage +vendor/bin/phpunit -v --coverage-html coverage/ + +# Run specific test suites +vendor/bin/phpunit --testsuite unit # Unit tests (fast) +vendor/bin/phpunit --testsuite kernel # Kernel tests +vendor/bin/phpunit --testsuite functional # Functional tests (slower) +vendor/bin/phpunit --testsuite javascript # JavaScript tests + +# Run specific tests +vendor/bin/phpunit --filter MyModuleUnitTest +vendor/bin/phpunit web/modules/custom/my_module/tests/src/Unit/ + +# Run with custom configuration +SIMPLETEST_DB=sqlite://localhost/tmp.sqlite vendor/bin/phpunit +``` + +### Unit Test Example + +```php +// tests/src/Unit/MyServiceTest.php +namespace Drupal\Tests\my_module\Unit; + +use Drupal\Tests\UnitTestCase; +use Drupal\my_module\Service\MyService; +use Prophecy\PhpUnit\ProphecyTrait; + +class MyServiceTest extends UnitTestCase { + use ProphecyTrait; + + public function testProcessReturnsExpectedValue(): void { + $logger = $this->prophesize('\Psr\Log\LoggerInterface'); + $service = new MyService($logger->reveal()); + $result = $service->process('input'); + $this->assertEquals('expected_output', $result); + } + + public function testProcessThrowsOnEmptyInput(): void { + $logger = $this->prophesize('\Psr\Log\LoggerInterface'); + $service = new MyService($logger->reveal()); + $this->expectException(\InvalidArgumentException::class); + $service->process(''); + } +} +``` + +### Kernel Test Example + +```php +// tests/src/Kernel/MyModuleKernelTest.php +namespace Drupal\Tests\my_module\Kernel; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\node\Entity\Node; +use Drupal\node\Entity\NodeType; + +class MyModuleKernelTest extends KernelTestBase { + + protected static $modules = ['system', 'node', 'user', 'my_module']; + + protected function setUp(): void { + parent::setUp(); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installConfig(['my_module']); + + // Create a node type for testing. + NodeType::create(['type' => 'article', 'name' => 'Article'])->save(); + } + + public function testNodeCreation(): void { + $node = Node::create([ + 'type' => 'article', + 'title' => 'Test Article', + 'status' => 1, + ]); + $node->save(); + + $this->assertNotNull($node->id()); + $this->assertEquals('article', $node->bundle()); + } +} +``` + +### Functional Test Example + +```php +// tests/src/Functional/MyModuleFunctionalTest.php +namespace Drupal\Tests\my_module\Functional; + +use Drupal\Tests\BrowserTestBase; + +class MyModuleFunctionalTest extends BrowserTestBase { + + protected $defaultTheme = 'stark'; + protected static $modules = ['node', 'my_module']; + + protected function setUp(): void { + parent::setUp(); + $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + $user = $this->drupalCreateUser(['access content', 'create article content']); + $this->drupalLogin($user); + } + + public function testArticleCreation(): void { + $this->drupalGet('/node/add/article'); + $this->assertSession()->statusCodeEquals(200); + + $edit = [ + 'title[0][value]' => 'Test Article Title', + ]; + $this->submitForm($edit, 'Save'); + $this->assertSession()->pageTextContains('Article Test Article Title has been created.'); + } + + public function testMyModulePageAccess(): void { + $this->drupalGet('/my-module/custom/1'); + $this->assertSession()->statusCodeEquals(403); + } +} +``` + +### Code Quality Tools + +```bash +# Static analysis +vendor/bin/phpstan analyse +vendor/bin/psalm + +# Security scanning +vendor/bin/drupal-check +composer audit + +# Accessibility testing +vendor/bin/phpunit --group accessibility +``` + +### Before Submitting Code + +```bash +# Quality checklist +vendor/bin/phpcs --standard=Drupal . +vendor/bin/phpunit +drush cr +drush updatedb +``` + +## Lagoon Development Workflow + +### Project Structure + +- **Modules** → `web/modules/custom/` +- **Themes** → `web/themes/custom/` +- **Configuration** → Export with `drush config:export` +- **Profiles** → `web/profiles/custom/` +- **Lagoon config** → `.lagoon.yml` in project root +- **Docker Compose** → `docker-compose.yml` in project root + +### Deployment Workflow + +```bash +# Feature development workflow +git checkout -b feature/my-feature +# ... make changes ... +git push origin feature/my-feature +# Lagoon auto-deploys the branch as a new environment + +# Check deployment status +lagoon get environment --project --environment feature-my-feature + +# View deployment logs +lagoon logs --project --environment feature-my-feature + +# After review, merge to main +git checkout main +git merge feature/my-feature +git push origin main +# Lagoon auto-deploys to production +``` + +### Remote Drush Commands + +```bash +# Run Drush on a remote Lagoon environment +drush @lagoon.main status +drush @lagoon.main config:export +drush @lagoon.main config:import --yes +drush @lagoon.main cache:rebuild +drush @lagoon.main updatedb +drush @lagoon.main pm:enable my_module + +# Sync production DB to local for development +lagoon-sync sync mariadb -p -e main -t local +drush cr # Rebuild caches after sync + +# Sync files from production +lagoon-sync sync files -p -e main -t local +``` + +### Version Control Workflow + +- **Commit messages**: Format `[#123456] Brief descriptive title` +- **Branch from**: `main` branch for features (auto-deployed by Lagoon) +- **Atomic commits**: One logical change per commit +- **Before pushing**: Run linting and tests locally + +## Advanced Development Patterns + +### Events & EventSubscribers + +Prefer EventSubscribers over hooks for many use cases. They are more testable and follow Symfony conventions. + +**EventSubscriber example**: + +```php +namespace Drupal\my_module\EventSubscriber; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Event\RequestEvent; + +class MyEventSubscriber implements EventSubscriberInterface { + + public static function getSubscribedEvents(): array { + return [ + KernelEvents::REQUEST => ['onKernelRequest', 100], + KernelEvents::RESPONSE => ['onKernelResponse'], + ]; + } + + public function onKernelRequest(RequestEvent $event): void { + $request = $event->getRequest(); + // ... + } + + public function onKernelResponse(\Symfony\Component\HttpKernel\Event\ResponseEvent $event): void { + // Modify response. + } +} +``` + +Register in `my_module.services.yml`: + +```yaml +my_module.event_subscriber: + class: Drupal\my_module\EventSubscriber\MyEventSubscriber + tags: + - { name: event_subscriber } +``` + +### Configuration Management + +**Config schema** (`config/schema/my_module.schema.yml`): + +```yaml +my_module.settings: + type: config_object + label: "My Module settings" + mapping: + api_key: + type: string + label: "API Key" + max_items: + type: integer + label: "Maximum items" + enabled_types: + type: sequence + label: "Enabled content types" + sequence: + type: string + label: "Content type" +``` + +**Config install** (`config/install/my_module.settings.yml`): + +```yaml +api_key: "" +max_items: 50 +enabled_types: + - article +``` + +**Reading config**: + +```php +// In a service/controller (injected) +$value = $this->configFactory->get('my_module.settings')->get('api_key'); + +// In a .module file (less preferred) +$value = \Drupal::config('my_module.settings')->get('api_key'); +``` + +**Config workflow**: + +```bash +# Export all configuration +drush config:export + +# Import configuration +drush config:import + +# View a single config value +drush config:get system.site + +# Edit config interactively +drush config:edit my_module.settings +``` + +- **`config/install/`**: Required config installed when module is enabled +- **`config/optional/`**: Config installed only if dependencies are met +- **Config split**: Use `config_split` module for per-environment configuration (dev/staging/prod) + +### Batch API for Long Operations + +```php +use Drupal\Core\Batch\BatchBuilder; + +function my_module_process_items(array $items): void { + $batch = (new BatchBuilder()) + ->setTitle(t('Processing items')) + ->setInitMessage(t('Initializing...')) + ->setProgressMessage(t('Processed @current out of @total.')) + ->setErrorMessage(t('An error occurred during processing.')) + ->setFile(\Drupal::service('extension.list.module')->getPath('my_module') . '/my_module.batch.inc') + ->setFinishCallback('my_module_batch_finished') + ->addOperation('my_module_batch_process', [$items]); + + batch_set($batch->toArray()); +} + +// In my_module.batch.inc +function my_module_batch_process(array $items, array &$context): void { + if (!isset($context['sandbox']['progress'])) { + $context['sandbox']['progress'] = 0; + $context['sandbox']['max'] = count($items); + $context['sandbox']['items'] = $items; + } + + $limit = 10; + $items_to_process = array_slice($context['sandbox']['items'], $context['sandbox']['progress'], $limit); + + foreach ($items_to_process as $item) { + // Process each item. + $context['sandbox']['progress']++; + } + + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; + $context['message'] = t('Processed @progress of @max items', [ + '@progress' => $context['sandbox']['progress'], + '@max' => $context['sandbox']['max'], + ]); +} + +function my_module_batch_finished(bool $success, array $results, array $operations): void { + $messenger = \Drupal::messenger(); + if ($success) { + $messenger->addStatus(t('Batch completed successfully.')); + } + else { + $messenger->addError(t('An error occurred during batch processing.')); + } +} +``` + +### Queue API for Background Processing + +```php +namespace Drupal\my_module\Plugin\QueueWorker; + +use Drupal\Core\Queue\QueueWorkerBase; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Processes my module items. + * + * @QueueWorker( + * id = "my_module_processor", + * title = @Translation("My Module Processor"), + * cron = {"time" = 60} + * ) + */ +class MyQueueWorker extends QueueWorkerBase implements ContainerFactoryPluginInterface { + + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { + return new static($configuration, $plugin_id, $plugin_definition); + } + + public function processItem($data): void { + if (!isset($data['type'])) { + throw new \InvalidArgumentException('Missing type in queue item.'); + } + // Do work... + } +} +``` + +### AJAX Forms + +- **Trigger elements**: Add `#ajax` property to form elements (select, checkbox, button) +- **Callback method**: Reference callback method using `::methodName` syntax +- **Wrapper element**: Specify target element ID for AJAX response replacement +- **Error handling**: Implement try-catch blocks in AJAX callbacks +- **Form state**: Use `$form_state->getTriggeringElement()` to identify trigger + +### Render API Deep Dive + +```php +$build = [ + '#type' => 'container', + '#attributes' => ['class' => ['my-wrapper']], + 'heading' => [ + '#type' => 'html_tag', + '#tag' => 'h2', + '#value' => $this->t('My Heading'), + ], + 'content' => [ + '#theme' => 'item_list', + '#items' => $items, + '#empty' => $this->t('No items found.'), + ], + '#cache' => [ + 'keys' => ['my_module', 'list', $categoryId], + 'tags' => ['node_list', 'taxonomy_term:' . $categoryId], + 'contexts' => ['user.roles', 'languages:language_content'], + 'max-age' => 3600, + ], + '#attached' => [ + 'library' => ['my_module/my_module.styles'], + 'drupalSettings' => [ + 'my_module' => ['endpoint' => '/api/items'], + ], + ], +]; +``` + +### Migration API + +```yaml +# migrations/my_migration.yml +source: + plugin: csv + path: /path/to/data.csv + header_row_count: 1 + keys: + - id + +process: + title: title + body/value: body + body/format: + plugin: default_value + default_value: basic_html + type: + plugin: default_value + default_value: article + +destination: + plugin: entity:node + default_bundle: article +``` + +### Composer Management + +```bash +# Add a module +composer require drupal/admin_toolbar + +# Update Drupal core +composer update drupal/core --with-all-dependencies + +# Run post-install steps +drush updatedb +drush config:import +drush cr +``` + +### JavaScript & Frontend + +**Drupal behaviors**: + +```javascript +(function (Drupal, drupalSettings) { + "use strict"; + + Drupal.behaviors.myModuleBehavior = { + attach: function (context, settings) { + const elements = context.querySelectorAll(".my-element"); + elements.forEach(function (element) { + element.addEventListener("click", handleClick); + }); + }, + detach: function (context, settings, trigger) { + const elements = context.querySelectorAll(".my-element"); + elements.forEach(function (element) { + element.removeEventListener("click", handleClick); + }); + }, + }; +})(Drupal, drupalSettings); +``` + +## Troubleshooting + +### Lagoon Deployment Issues + +```bash +# Check deployment status +lagoon get environment --project --environment + +# View deployment logs +lagoon logs --project --environment + +# SSH into a pod for debugging +lagoon ssh --project --environment + +# Check remote Drush status +drush @lagoon. status +``` + +### Database Sync Issues + +```bash +# If lagoon-sync fails, try Drush +drush sql:sync @lagoon.main @self + +# Clear caches after sync +drush cr +``` + +### Performance Issues + +```bash +# Check remote cache settings +drush @lagoon.main config:get system.performance + +# Check watchdog for errors +drush @lagoon.main watchdog:show --severity=Error + +# Check Redis connection +drush @lagoon.main php:eval "var_dump(\Drupal::service('cache.default')->get('test'));" +``` + +## Additional Resources + +### Lagoon Documentation + +- **Lagoon Docs**: https://docs.lagoon.sh +- **Lagoon CLI**: https://github.com/uselagoon/lagoon-cli +- **lagoon-sync**: https://github.com/uselagoon/lagoon-sync +- **Drupal on Lagoon**: https://docs.lagoon.sh/lagoon/using-lagoon-the-basics/drupal/ + +### Drupal Documentation + +- **Drupal API**: https://api.drupal.org +- **Developer Guide**: https://www.drupal.org/docs/develop +- **Coding Standards**: https://www.drupal.org/docs/develop/standards +- **Security Best Practices**: https://www.drupal.org/docs/develop/security + +### Community Resources + +- **amazee.io Blog**: https://amazee.io/blog +- **DrupalAtYourFingertips**: https://www.drupalatyourfingertips.com +- **Drupal Answers**: https://drupal.stackexchange.com +- **Drupal Slack**: https://drupal.slack.com diff --git a/README.md b/README.md index dd4367c..019d9b8 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,14 @@ This repository contains specialized AGENTS.md files designed for AI coding agents working on Drupal projects. These guides provide comprehensive instructions for Drupal development following modern best practices. -> **⚠️ Warning: Work in Progress** -> This is an evolving project. The guides are actively being refined and updated. Use with caution and always test in development environments. +> **🧪 Try the [Experimental Knowledge Base Architecture](./experimental/)** — A new slim AGENTS.md + `.kb/` folder pattern that loads only what the agent needs, cutting token usage from ~8,000 to ~400 for simple tasks. [Learn more →](./experimental/README.md) + +> **⚠️ Warning: Work in Progress** This is an evolving project. The guides are actively being refined and updated. Use with caution and always test in development environments. ## Table of Contents - [Available Guides](#available-guides) +- [Version Compatibility](#version-compatibility) - [What's Included](#whats-included) - [How to Use AGENTS.md?](#how-to-use-agentsmd) - [Key Features](#key-features) @@ -18,6 +20,7 @@ This repository contains specialized AGENTS.md files designed for AI coding agen ## Available Guides ### 🐳 [DDEV/AGENTS.md](./DDEV/AGENTS.md) + **For Docker-based development with DDEV** - **Environment**: DDEV (Docker-based local development) @@ -27,6 +30,7 @@ This repository contains specialized AGENTS.md files designed for AI coding agen - **Best for**: Modern containerized development environments ### 🖥️ [Vanilla/AGENTS.md](./Vanilla/AGENTS.md) + **For traditional server-based development** - **Environment**: Traditional LAMP/LEMP stack @@ -35,100 +39,144 @@ This repository contains specialized AGENTS.md files designed for AI coding agen - **Features**: Traditional server configuration, direct file system access - **Best for**: Classic server environments, hosting providers, manual infrastructure +### ☸️ [Lagoon/AGENTS.md](./Lagoon/AGENTS.md) + +**For amazee.io Lagoon (Kubernetes-based hosting)** + +- **Environment**: amazee.io Lagoon with Kubernetes +- **Setup**: Lagoon CLI, lagoon-sync, Drush aliases +- **Commands**: Lagoon CLI + Drush alias commands +- **Features**: Auto-deployment, environment variables, Varnish, Redis, post-rollout tasks +- **Best for**: Projects hosted on amazee.io Lagoon or self-hosted Lagoon + +## Version Compatibility + +| AGENTS.md Variant | Drupal | PHP | Drush | Special Requirements | +| ----------------- | ----------- | ---- | ----- | ----------------------- | +| DDEV | 10.x / 11.x | 8.3+ | 13+ | DDEV 1.23+ | +| Vanilla | 10.x / 11.x | 8.3+ | 13+ | LAMP/LEMP stack | +| Lagoon | 10.x / 11.x | 8.3+ | 13+ | Lagoon CLI, lagoon-sync | + ## What's Included Each AGENTS.md file contains: -### 📚 **Development Patterns** +### 📚 **Development Patterns** (with code examples) + - Services & Dependency Injection - Entity API & Queries - Plugin System - Hooks Implementation -- Forms API +- Forms API (simple + config forms) - Routes & Controllers +- Access Control - Batch API & Queue API - AJAX Forms +- Events & EventSubscribers +- Render API +- Migration API +- Configuration Management +- Composer Management +- JavaScript & Drupal Behaviors +- Content Moderation & Workflows ### 🛡️ **Security & Performance** + - Security best practices - Performance optimization -- Caching strategies -- Render caching techniques +- Caching strategies (render, Varnish, Redis) +- Lazy builders and placeholder strategies + +### 🧪 **Testing & Quality** (with code examples) -### 🧪 **Testing & Quality** - PHPUnit testing framework -- Unit, Kernel, and Functional tests -- Code quality tools +- Unit, Kernel, and Functional test stubs +- Code quality tools (PHPStan, Psalm, PHPCS) - JavaScript testing +### 🚫 **Anti-Patterns** + +- 14 common mistakes to avoid +- Clear "Never Do This" guidelines + ### 🔧 **Development Workflow** -- Essential commands -- Debugging tools + +- Module scaffolding template with full file structure +- Environment-specific commands +- Debugging tools and tables - Performance profiling - Troubleshooting common issues -### 📋 **Best Practices** -- Drupal coding standards -- Version control workflow -- Pull request guidelines - ## How to Use AGENTS.md? -1. **Get the repository** - Download or clone this repository to your computer +1. **Get the repository** Download or clone this repository to your computer -2. **Extract the files** (if downloading) - Extract the downloaded ZIP file and open the folder +2. **Extract the files** (if downloading) Extract the downloaded ZIP file and open the folder 3. **Copy the right AGENTS.md file** - If you use DDEV for development: - Navigate to the `DDEV` folder - Copy the `AGENTS.md` file - Paste it in your Drupal project's main folder - - If you use traditional server setup: + - If you use a traditional server setup: - Navigate to the `Vanilla` folder - Copy the `AGENTS.md` file - Paste it in your Drupal project's main folder + - If you use amazee.io Lagoon: + - Navigate to the `Lagoon` folder + - Copy the `AGENTS.md` file + - Paste it in your Drupal project's main folder + +4. **Start your AI agent** — Open your AI coding tool (Cursor, Claude Code, etc.) in the project directory. It will automatically read the AGENTS.md file. ## Key Features ### ✅ **What We Provide** -- Comprehensive Drupal development patterns -- Environment-specific instructions + +- Comprehensive Drupal development patterns with concrete code examples +- Environment-specific instructions (DDEV, Vanilla, Lagoon) - Security and performance guidelines -- Testing strategies and quality assurance -- Troubleshooting common issues +- Testing strategies with complete test class stubs +- Anti-patterns section ("Never Do This") +- Full module scaffolding template +- Configuration management guidance +- Migration API examples +- Troubleshooting guides ### ❌ **What We Don't Include** + - Infrastructure setup tutorials - Server configuration details -- Environment variable examples +- Basic Drupal installation guides - Apache/Nginx configuration -- Basic Drupal installation +- Drupal 7/8/9 specific guidance ## Architecture The guides follow the [agents.md](https://agents.md) standard format: + - **Simple, open format** for AI coding agents - **Living documentation** that evolves with Drupal - **Environment-specific versions** for different setups -- **Pattern-focused content** rather than code snippets +- **Code-first content** with concrete, copy-pasteable examples +- **Internal table of contents** for quick navigation within each guide ## Contributing -This is a work in progress. Areas for improvement: -- Additional development patterns -- Environment-specific optimizations -- Real-world examples and use cases -- Integration with modern development tools +See [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed guidelines on how to contribute to this project. + +## Changelog + +See [CHANGELOG.md](./CHANGELOG.md) for a history of changes. ## Resources - **Drupal Documentation**: [drupal.org/docs](https://www.drupal.org/docs) - **Drupal API**: [api.drupal.org](https://api.drupal.org) - **DrupalAtYourFingertips**: [drupalatyourfingertips.com](https://www.drupalatyourfingertips.com) +- **amazee.io Docs**: [docs.lagoon.sh](https://docs.lagoon.sh) - **agents.md Standard**: [agents.md](https://agents.md) --- -**Note**: These guides focus on Drupal 10.x+ development patterns and modern best practices. Always adapt instructions to your specific project requirements and environment constraints. +**Note**: These guides focus on Drupal 10.x+ and 11.x development patterns and modern best practices. Always adapt instructions to your specific project requirements and environment constraints. diff --git a/Vanilla/AGENTS.md b/Vanilla/AGENTS.md index d661599..76a2337 100644 --- a/Vanilla/AGENTS.md +++ b/Vanilla/AGENTS.md @@ -2,28 +2,150 @@ **AI Agent Instructions**: This guide provides comprehensive instructions for AI coding agents working on Drupal projects using traditional server setups. Follow these guidelines for consistent, high-quality contributions. Human contributors should use README.md instead. +## Table of Contents + +- [Project Overview](#project-overview) +- [Prerequisites](#prerequisites) +- [Module Scaffolding Template](#module-scaffolding-template) +- [Code Style and Standards](#code-style-and-standards) +- [Drupal Development Patterns](#drupal-development-patterns) +- [Security & Performance Guidelines](#security--performance-guidelines) +- [Anti-Patterns — Never Do This](#anti-patterns--never-do-this) +- [Testing & Quality Assurance](#testing--quality-assurance) +- [Development Workflow](#development-workflow) +- [Advanced Development Patterns](#advanced-development-patterns) +- [Additional Topics](#additional-topics) +- [Troubleshooting](#troubleshooting) +- [Additional Resources](#additional-resources) + ## Project Overview -- **Core Technology**: Drupal 10.x+ (verify via `composer show drupal/core`) + +- **Core Technology**: Drupal 10.x / 11.x (verify via `composer show drupal/core`) - **Key Components**: Custom modules, themes, configuration management, Composer dependencies -- **Environment**: PHP 8.1+, MySQL/PostgreSQL 8.0+, Apache/Nginx -- **Development Tools**: Composer, Drush 12+, Git +- **Environment**: PHP 8.3+, MySQL/PostgreSQL, Apache/Nginx +- **Development Tools**: Composer, Drush 13+, Git - **Important**: Always run commands from project root unless specified -## Quick Setup Commands +## Prerequisites -### Prerequisites ```bash # System requirements -PHP 8.1+ with required extensions +PHP 8.3+ with required extensions (gd, xml, mbstring, json, pdo, curl, zip) MySQL 8.0+ or PostgreSQL 12+ Apache 2.4+ or Nginx 1.18+ Composer 2.0+ -Drush 12+ +Drush 13+ + +# Verify requirements +php -v +composer --version +drush --version +php -m # Check installed extensions +``` + +## Module Scaffolding Template + +When creating a new custom module, follow this structure: + ``` +modules/custom/my_module/ +├── my_module.info.yml # Module metadata (required) +├── my_module.module # Hook implementations +├── my_module.routing.yml # Route definitions +├── my_module.services.yml # Service definitions +├── my_module.permissions.yml # Permission definitions +├── my_module.links.menu.yml # Menu links +├── my_module.links.action.yml # Action links +├── my_module.links.task.yml # Task (tab) links +├── my_module.libraries.yml # CSS/JS libraries +├── my_module.routing.yml # Route definitions +├── composer.json # PSR-4 autoloading +├── src/ +│ ├── Controller/ +│ │ └── MyController.php +│ ├── Form/ +│ │ ├── SettingsForm.php # ConfigFormBase +│ │ └── CustomForm.php # FormBase +│ ├── Plugin/ +│ │ ├── Block/ +│ │ │ └── MyBlock.php +│ │ ├── Field/ +│ │ │ ├── FieldFormatter/ +│ │ │ │ └── MyFormatter.php +│ │ │ ├── FieldWidget/ +│ │ │ │ └── MyWidget.php +│ │ │ └── FieldType/ +│ │ │ └── MyFieldItem.php +│ │ └── QueueWorker/ +│ │ └── MyQueueWorker.php +│ ├── EventSubscriber/ +│ │ └── MyEventSubscriber.php +│ ├── Access/ +│ │ └── MyAccessChecker.php +│ ├── Entity/ +│ │ └── MyEntity.php +│ └── Service/ +│ └── MyService.php +├── config/ +│ ├── install/ # Config installed with module +│ │ └── my_module.settings.yml +│ ├── optional/ # Config installed only if dependencies met +│ │ └── field.field.node.article.field_my_field.yml +│ └── schema/ # Config schema for typed data +│ └── my_module.schema.yml +├── templates/ +│ └── my-module-template.html.twig +├── css/ +│ └── my-module.css +├── js/ +│ └── my-module.js +└── tests/ + └── src/ + ├── Unit/ + │ └── MyServiceTest.php + ├── Kernel/ + │ └── MyModuleKernelTest.php + └── Functional/ + └── MyModuleFunctionalTest.php +``` + +### Minimal Module Files +**my_module.info.yml**: +```yaml +name: "My Module" +type: module +description: "Custom module description." +core_version_requirement: ^10 || ^11 +package: Custom +dependencies: + - drupal:node + - drupal:user +``` + +**composer.json** (for PSR-4 autoloading in tests): + +```json +{ + "name": "drupal/my_module", + "type": "drupal-custom-module", + "description": "Custom module description.", + "autoload": { + "psr-4": { + "Drupal\\my_module\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Drupal\\Tests\\my_module\\": "tests/src/" + } + } +} +``` ## Code Style and Standards + Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and PHPCS for enforcement. - **PHP**: @@ -37,6 +159,7 @@ Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and - **Twig**: `{{ }}` for output, `{% %}` for logic; always escape with `|e` - **Linting**: + ```bash vendor/bin/phpcs --standard=Drupal --extensions=php,inc,module,install,info,yml src/ vendor/bin/phpcs --standard=DrupalPractice --extensions=php,inc,module,install,info,yml src/ @@ -48,85 +171,505 @@ Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and ## Drupal Development Patterns ### Services & Dependency Injection -- **Create services** in `modulename.services.yml` file for reusable logic -- **Use dependency injection** to inject services into controllers, forms, and plugins -- **Core services** like `@current_user`, `@entity_type.manager`, `@database` are available + +**Create services** in `modulename.services.yml`: + +```yaml +# my_module.services.yml +services: + my_module.my_service: + class: Drupal\my_module\Service\MyService + arguments: ["@entity_type.manager", "@logger.factory", "@config.factory"] + tags: + - { name: backend_overridable } +``` + +**Use dependency injection** in controllers, forms, and plugins: + +```php +namespace Drupal\my_module\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +class MyController extends ControllerBase { + + public function __construct( + protected EntityTypeManagerInterface $entityTypeManager, + ) {} + + public static function create(ContainerInterface $container): static { + return new static( + $container->get('entity_type.manager'), + ); + } + + public function content(): array { + $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple(); + // ... + return $build; + } +} +``` + +- **Core services** like `@current_user`, `@entity_type.manager`, `@database`, `@config.factory`, `@logger.factory` are available - **Best practice**: Avoid static `\Drupal::` calls in favor of dependency injection -- **Service discovery**: Use `drush eval "print_r(\Drupal::getContainer()->getServiceIds());"` to see available services +- **Service discovery**: Use `drush php:eval "print_r(\Drupal::getContainer()->getServiceIds());"` to see available services - **Location**: Place service classes in `src/` directory with proper namespace ### Entity API & Queries -- **Entity loading**: Use `Entity::load($id)` for single entities or `entityTypeManager()->getStorage()` for multiple -- **Entity queries**: Use `\Drupal::entityQuery()` for database operations instead of raw SQL -- **Query conditions**: Chain multiple conditions with `->condition()`, `->sort()`, `->range()` -- **Entity creation**: Create entities with `Entity::create(['type' => 'bundle_name'])` -- **Field access**: Use entity field API instead of direct property access -- **Performance**: Use entity query cache tags and contexts for optimal caching + +**Loading entities**: + +```php +// Single entity +$node = \Drupal::entityTypeManager()->getStorage('node')->load(123); + +// Multiple entities +$nodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple([1, 2, 3]); + +// Load by properties +$nodes = \Drupal::entityTypeManager()->getStorage('node')->loadByProperties([ + 'type' => 'article', + 'status' => 1, +]); +``` + +**Entity queries** (always prefer over raw SQL): + +```php +use Drupal\Core\Entity\Query\QueryInterface; + +// Modern entity query with injected service +$ids = $this->entityTypeManager->getStorage('node')->getQuery() + ->condition('type', 'article') + ->condition('status', 1) + ->condition('field_category', $categoryId) + ->sort('created', 'DESC') + ->range(0, 10) + ->accessCheck(TRUE) // ALWAYS set explicitly in Drupal 10.2+ + ->execute(); + +$nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($ids); +``` + +**Creating entities**: + +```php +$node = \Drupal::entityTypeManager()->getStorage('node')->create([ + 'type' => 'article', + 'title' => 'My Article', + 'body' => [ + 'value' => 'Content here', + 'format' => 'full_html', + ], + 'status' => 1, + 'uid' => 1, +]); +$node->save(); +``` + +**Field access**: Use entity field API instead of direct property access: + +```php +// Correct +$node->get('field_my_field')->value; +$node->get('field_my_field')->entity; // For entity reference fields + +// Avoid +$node->field_my_field->value; // Magic __get — works but less explicit +``` ### Plugin System -- **Plugin types**: Blocks, field formatters, field widgets, menu links, and more -- **Plugin discovery**: Use annotation-based discovery in docblocks -- **Plugin configuration**: Define plugin ID, label, and other metadata in annotations -- **Plugin base classes**: Extend appropriate base classes (BlockBase, FormatterBase, etc.) + +**Block plugin example**: + +```php +namespace Drupal\my_module\Plugin\Block; + +use Drupal\Core\Block\BlockBase; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides a 'My Custom Block' block. + * + * @Block( + * id = "my_custom_block", + * admin_label = @Translation("My Custom Block"), + * category = @Translation("Custom"), + * context_definitions = { + * "node" = @ContextDefinition("entity:node", label = @Translation("Node")) + * } + * ) + */ +class MyCustomBlock extends BlockBase implements ContainerFactoryPluginInterface { + + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { + return new static($configuration, $plugin_id, $plugin_definition); + } + + public function build(): array { + return [ + '#markup' => $this->t('Hello from my custom block!'), + '#cache' => [ + 'tags' => ['node_list'], + 'contexts' => ['user.roles'], + ], + ]; + } +} +``` + +- **Plugin types**: Blocks, field formatters, field widgets, field types, menu links, QueueWorker, Condition, Action, and more +- **Plugin discovery**: Use annotation-based discovery (as shown above) or YAML discovery +- **Plugin base classes**: Extend appropriate base classes (`BlockBase`, `FormatterBase`, `WidgetBase`, etc.) - **Plugin placement**: Place plugins in `src/Plugin/Type/` directory structure -- **Derivative plugins**: Use for creating multiple plugins from one definition +- **Derivative plugins**: Use `DeriverBase` for creating multiple plugins from one definition ### Hooks -- **Hook implementation**: Implement hooks in `modulename.module` file + +**Implement hooks** in `modulename.module` file: + +```php +/** + * Implements hook_form_alter(). + */ +function my_module_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id): void { + if ($form_id === 'node_article_form') { + $form['title']['#title'] = t('Article Title'); + $form['actions']['submit']['#value'] = t('Publish Article'); + } +} + +/** + * Implements hook_theme(). + */ +function my_module_theme($existing, $type, $theme, $path): array { + return [ + 'my_template' => [ + 'variables' => [ + 'title' => '', + 'items' => [], + ], + 'template' => 'my-template', + ], + ]; +} + +/** + * Implements hook_entity_presave(). + */ +function my_module_entity_presave(\Drupal\Core\Entity\EntityInterface $entity): void { + if ($entity->getEntityTypeId() === 'node' && $entity->bundle() === 'article') { + // Auto-set a field before saving. + $entity->set('field_last_updated', \Drupal::time()->getRequestTime()); + } +} + +/** + * Implements hook_cron(). + */ +function my_module_cron(): void { + // Process items during cron runs. + \Drupal::queue('my_module_processor')->createItem(['type' => 'cleanup']); +} +``` + - **Hook naming**: Follow pattern `hook_modulename_action()` for custom hooks - **Hook parameters**: Use type hints and proper parameter documentation -- **Core hooks**: Common hooks include `hook_form_alter()`, `hook_theme()`, `hook_menu_links_discovered_alter()` +- **Core hooks**: Common hooks include `hook_form_alter()`, `hook_theme()`, `hook_entity_presave()`, `hook_cron()`, `hook_menu_links_discovered_alter()` - **Hook order**: Hooks fire in module weight order (lowest first) -- **Best practice**: Keep hook implementations focused and use services for complex logic +- **Best practice**: Keep hook implementations focused and delegate complex logic to services ### Forms API -- **Form classes**: Extend `FormBase` for simple forms or `ConfigFormBase` for configuration forms -- **Form structure**: Use render array structure with `#type`, `#title`, `#description` properties -- **Form validation**: Implement `validateForm()` method for custom validation -- **Form submission**: Implement `submitForm()` method for processing form data -- **Form elements**: Use proper form element types (textfield, select, checkbox, etc.) -- **AJAX forms**: Add `#ajax` property to form elements for dynamic behavior -- **Form caching**: Forms are automatically cached with CSRF protection + +**Simple form**: + +```php +namespace Drupal\my_module\Form; + +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; + +class CustomForm extends FormBase { + + public function getFormId(): string { + return 'my_module_custom_form'; + } + + public function buildForm(array $form, FormStateInterface $form_state): array { + $form['email'] = [ + '#type' => 'email', + '#title' => $this->t('Email Address'), + '#required' => TRUE, + ]; + + $form['category'] = [ + '#type' => 'select', + '#title' => $this->t('Category'), + '#options' => [ + 'news' => $this->t('News'), + 'events' => $this->t('Events'), + 'blog' => $this->t('Blog'), + ], + '#ajax' => [ + 'callback' => '::categoryChanged', + 'wrapper' => 'subcategory-wrapper', + 'event' => 'change', + ], + ]; + + $form['subcategory'] = [ + '#type' => 'container', + '#attributes' => ['id' => 'subcategory-wrapper'], + 'value' => [ + '#type' => 'textfield', + '#title' => $this->t('Subcategory'), + ], + ]; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + public function categoryChanged(array &$form, FormStateInterface $form_state): array { + return $form['subcategory']; + } + + public function validateForm(array &$form, FormStateInterface $form_state): void { + $email = $form_state->getValue('email'); + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + $form_state->setErrorByName('email', $this->t('Please enter a valid email address.')); + } + } + + public function submitForm(array &$form, FormStateInterface $form_state): void { + $this->messenger()->addStatus($this->t('Form submitted successfully.')); + $form_state->setRedirect(''); + } +} +``` + +**Configuration form**: + +```php +namespace Drupal\my_module\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; + +class SettingsForm extends ConfigFormBase { + + public function getFormId(): string { + return 'my_module_settings'; + } + + protected function getEditableConfigNames(): array { + return ['my_module.settings']; + } + + public function buildForm(array $form, FormStateInterface $form_state): array { + $config = $this->config('my_module.settings'); + + $form['api_key'] = [ + '#type' => 'textfield', + '#title' => $this->t('API Key'), + '#default_value' => $config->get('api_key'), + '#required' => TRUE, + ]; + + $form['max_items'] = [ + '#type' => 'number', + '#title' => $this->t('Maximum Items'), + '#default_value' => $config->get('max_items') ?? 50, + '#min' => 1, + '#max' => 500, + ]; + + return parent::buildForm($form, $form_state); + } + + public function submitForm(array &$form, FormStateInterface $form_state): void { + $this->config('my_module.settings') + ->set('api_key', $form_state->getValue('api_key')) + ->set('max_items', $form_state->getValue('max_items')) + ->save(); + + parent::submitForm($form, $form_state); + } +} +``` ### Routes & Controllers -- **Routing file**: Define routes in `modulename.routing.yml` with path, defaults, and requirements -- **Controllers**: Create controller classes extending `ControllerBase` in `src/Controller/` -- **Route parameters**: Use `{parameter}` placeholders in paths and inject into controller methods -- **Access control**: Implement `_permission`, `_role`, or custom access callbacks -- **Route naming**: Use `modulename.action` naming convention for clarity -- **Controller injection**: Use constructor injection for dependencies -- **Return values**: Return render arrays or Symfony Response objects + +**Route definition** (`my_module.routing.yml`): + +```yaml +my_module.content: + path: "/my-module/{node}" + defaults: + _controller: '\Drupal\my_module\Controller\MyController::content' + _title: "My Module Page" + requirements: + _permission: "access content" + node: \d+ + +my_module.settings: + path: "/admin/config/my-module/settings" + defaults: + _form: '\Drupal\my_module\Form\SettingsForm' + _title: "My Module Settings" + requirements: + _permission: "administer site configuration" + +my_module.custom_access: + path: "/my-module/custom/{node}" + defaults: + _controller: '\Drupal\my_module\Controller\MyController::customPage' + _title_callback: '\Drupal\my_module\Controller\MyController::pageTitle' + requirements: + _custom_access: '\Drupal\my_module\Access\MyAccessChecker::access' +``` + +**Controller**: + +```php +namespace Drupal\my_module\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\node\NodeInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +class MyController extends ControllerBase { + + public function __construct( + protected EntityTypeManagerInterface $entityTypeManager, + ) {} + + public static function create(ContainerInterface $container): static { + return new static( + $container->get('entity_type.manager'), + ); + } + + public function content(NodeInterface $node): array { + return [ + '#theme' => 'my_template', + '#title' => $node->label(), + '#items' => $this->getItems($node), + '#cache' => [ + 'tags' => ['node:' . $node->id()], + 'contexts' => ['user.permissions'], + ], + '#attached' => [ + 'library' => ['my_module/my_module.styles'], + ], + ]; + } + + public function pageTitle(NodeInterface $node): string { + return $this->t('Page: @title', ['@title' => $node->label()]); + } +} +``` + +**Custom access checker**: + +```php +namespace Drupal\my_module\Access; + +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Routing\Access\AccessInterface; +use Drupal\node\NodeInterface; + +class MyAccessChecker implements AccessInterface { + + public function access(NodeInterface $node): AccessResult { + return AccessResult::allowedIf($node->isPublished()) + ->addCacheTags(['node:' . $node->id()]) + ->cachePerUser(); + } +} +``` + +Register in `my_module.services.yml`: + +```yaml +my_module.access_checker: + class: Drupal\my_module\Access\MyAccessChecker + tags: + - { name: access_check } +``` ## Security & Performance Guidelines ### Security Requirements + - **Always sanitize user input**: Use `#plain_text` for untrusted content - **CSRF protection**: Include `#token` for forms with side effects - **Permissions**: Implement proper access checks and route requirements - **SQL Injection**: Use Entity Query or proper parameter binding - **XSS Prevention**: Always use `|e` filter in Twig, `#markup` for trusted HTML only -- **File uploads**: Validate file types and sizes +- **File uploads**: Validate file types and sizes; use Drupal's file API - **Database credentials**: Never commit credentials to version control +- **Render arrays**: Never use `#markup` with unsanitized user input; use `#plain_text` or `check_plain()` ### Performance Best Practices + - **Render caching**: Always add `#cache` array to render arrays with appropriate `tags` and `contexts` - **Cache tags**: Use entity-based tags like `['node:123']` or list-based tags like `['node_list']` - **Cache contexts**: Apply user-specific contexts like `['user.roles']` for personalized content - **Lazy loading**: Use `#lazy_builder` for expensive operations that can be loaded separately -- **Placeholder strategy**: Set `#create_placeholder: TRUE` for lazy builders to improve initial page load +- **Placeholder strategy**: Set `#create_placeholder` => TRUE for lazy builders to improve initial page load - **Cache max-age**: Set appropriate `max-age` values based on content freshness requirements - **Avoid premature optimization**: Profile first, then optimize based on actual bottlenecks - **Database queries**: Use entity queries instead of raw SQL for better caching and security -- **Entity loading**: Load multiple entities at once when possible for better performance +- **Entity loading**: Load multiple entities at once with `loadMultiple()` instead of individual loads + +**Render array with caching**: + +```php +$build = [ + '#theme' => 'item_list', + '#items' => $items, + '#cache' => [ + 'keys' => ['my_module:item_list:' . $categoryId], + 'tags' => ['node_list', 'taxonomy_term:' . $categoryId], + 'contexts' => ['user.roles'], + 'max-age' => 3600, + ], +]; +``` + +**Lazy builder for expensive operations**: + +```php +$build['expensive_content'] = [ + '#lazy_builder' => [ + '\Drupal\my_module\Service\MyLazyBuilder::renderExpensiveContent', + [$param1, $param2], + ], + '#create_placeholder' => TRUE, +]; +``` ### Caching Strategies + - **Render cache**: Cache complex markup with proper tags/contexts -- **Dynamic page cache**: Configure for anonymous users -- **Internal page cache**: Enable for authenticated users -- **Entity cache**: Leverage core entity caching -- **Redis/Memcache**: Configure for distributed caching +- **Dynamic page cache**: Automatically handles cacheability for anonymous users +- **Internal page cache**: Serves full cached pages for anonymous users +- **Entity cache**: Core entity caching is automatic — invalidate with cache tags +- **Redis/Memcache**: Configure for distributed caching in production ### Server Optimization + ```bash # PHP configuration (php.ini) memory_limit = 256M @@ -139,15 +682,217 @@ innodb_buffer_pool_size = 1G query_cache_size = 64M ``` +## Anti-Patterns — Never Do This + +These are common mistakes that an AI agent must avoid: + +1. **Never use `\Drupal::` static calls in services, controllers, or plugins** — Use dependency injection instead. The only acceptable use is in `hook_` functions in `.module` files (and even there, consider delegating to a service). + +2. **Never query the database directly when Entity Query suffices** — Use `\Drupal::entityQuery()` or injected `$this->entityTypeManager->getStorage()->getQuery()`. + +3. **Never use `|raw` in Twig** — Use `|e` (or rely on auto-escaping). If you need raw HTML, use `#type => 'processed_text'` or `check_markup()`. + +4. **Never create monolithic `hook_form_alter()` functions** — If the alter logic is complex, delegate to a service. Break large hooks into focused helper methods. + +5. **Never store configuration in state that belongs in config** — State (`\Drupal::state()`) is for ephemeral/transient data (last cron run, temporary flags). Config (`\Drupal::configFactory()`) is for structured, exportable settings. + +6. **Never use `#markup` with unsanitized user input** — Always use `#plain_text` for untrusted content or `check_plain()` / `Html::escape()` for escaping. + +7. **Never skip `accessCheck(TRUE)` on entity queries** — Drupal 10.2+ requires explicit access checking on entity queries. Omitting it throws deprecation warnings and will be required in Drupal 12. + +8. **Never hardcode entity IDs, user IDs, or paths** — Use configuration, route names, and dynamic lookups instead. + +9. **Never use `hook_views_data()` without proper table aliases** — Always prefix table columns clearly to avoid SQL ambiguity. + +10. **Never ignore cacheability metadata** — Every render array that depends on data must specify `#cache` tags, contexts, and max-age. Missing cache metadata causes stale content or unnecessary cache invalidation. + +11. **Never commit `settings.php` with database credentials** — Use environment variables or `settings.local.php` (excluded from VCS). + +12. **Never use `node_load()` or other deprecated procedural functions** — Use the entity type manager: `\Drupal::entityTypeManager()->getStorage('node')->load()`. + +13. **Never use global variables like `$_GET`, `$_POST`, `$_SERVER`** — Use Symfony's `Request` object via dependency injection. + +14. **Never put business logic in `.module` files** — Delegate to services. The `.module` file should be thin: route hooks, theme hooks, and thin wrappers that call services. + +## Testing & Quality Assurance + +### PHPUnit Testing Framework + +Aim for ≥ 80% code coverage. Drupal provides multiple test types: + +```bash +# Run all tests with coverage +vendor/bin/phpunit -v --coverage-html coverage/ + +# Run specific test suites +vendor/bin/phpunit --testsuite unit # Unit tests (fast) +vendor/bin/phpunit --testsuite kernel # Kernel tests +vendor/bin/phpunit --testsuite functional # Functional tests (slower) +vendor/bin/phpunit --testsuite javascript # JavaScript tests + +# Run specific tests +vendor/bin/phpunit --filter MyModuleUnitTest +vendor/bin/phpunit modules/custom/my_module/tests/src/Unit/ + +# Run with custom configuration +SIMPLETEST_DB=sqlite://localhost/tmp.sqlite vendor/bin/phpunit +``` + +### Unit Test Example + +```php +// tests/src/Unit/MyServiceTest.php +namespace Drupal\Tests\my_module\Unit; + +use Drupal\Tests\UnitTestCase; +use Drupal\my_module\Service\MyService; +use Prophecy\PhpUnit\ProphecyTrait; + +class MyServiceTest extends UnitTestCase { + use ProphecyTrait; + + public function testProcessReturnsExpectedValue(): void { + $logger = $this->prophesize('\Psr\Log\LoggerInterface'); + $service = new MyService($logger->reveal()); + $result = $service->process('input'); + $this->assertEquals('expected_output', $result); + } + + public function testProcessThrowsOnEmptyInput(): void { + $logger = $this->prophesize('\Psr\Log\LoggerInterface'); + $service = new MyService($logger->reveal()); + $this->expectException(\InvalidArgumentException::class); + $service->process(''); + } +} +``` + +### Kernel Test Example + +```php +// tests/src/Kernel/MyModuleKernelTest.php +namespace Drupal\Tests\my_module\Kernel; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\node\Entity\Node; +use Drupal\node\Entity\NodeType; + +class MyModuleKernelTest extends KernelTestBase { + + protected static $modules = ['system', 'node', 'user', 'my_module']; + + protected function setUp(): void { + parent::setUp(); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installConfig(['my_module']); + + // Create a node type for testing. + NodeType::create(['type' => 'article', 'name' => 'Article'])->save(); + } + + public function testNodeCreation(): void { + $node = Node::create([ + 'type' => 'article', + 'title' => 'Test Article', + 'status' => 1, + ]); + $node->save(); + + $this->assertNotNull($node->id()); + $this->assertEquals('article', $node->bundle()); + } +} +``` + +### Functional Test Example + +```php +// tests/src/Functional/MyModuleFunctionalTest.php +namespace Drupal\Tests\my_module\Functional; + +use Drupal\Tests\BrowserTestBase; + +class MyModuleFunctionalTest extends BrowserTestBase { + + protected $defaultTheme = 'stark'; + protected static $modules = ['node', 'my_module']; + + protected function setUp(): void { + parent::setUp(); + // Create a test user with permissions. + $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + $user = $this->drupalCreateUser(['access content', 'create article content']); + $this->drupalLogin($user); + } + + public function testArticleCreation(): void { + $this->drupalGet('/node/add/article'); + $this->assertSession()->statusCodeEquals(200); + + // Submit the node form. + $edit = [ + 'title[0][value]' => 'Test Article Title', + ]; + $this->submitForm($edit, 'Save'); + $this->assertSession()->pageTextContains('Article Test Article Title has been created.'); + } + + public function testMyModulePageAccess(): void { + // Anonymous users should not access custom pages. + $this->drupalGet('/my-module/custom/1'); + $this->assertSession()->statusCodeEquals(403); + } +} +``` + +### Code Quality Tools + +```bash +# Static analysis (add to composer require) +vendor/bin/phpstan analyse # PHPStan analysis +vendor/bin/psalm # Psalm analysis + +# Security scanning +vendor/bin/drupal-check # Check for deprecated code +composer audit # Check for security advisories + +# Accessibility testing +vendor/bin/phpunit --group accessibility # Accessibility tests +``` + +### JavaScript Testing + +```bash +# Install JavaScript dependencies +npm install + +# Run JavaScript tests +npm run test # Jest tests +npm run test:a11y # Accessibility tests +``` + +### Before Submitting Code + +```bash +# Quality checklist +vendor/bin/phpcs --standard=Drupal . # Code style +vendor/bin/phpunit # Run tests +drush cr # Clear caches +drush updatedb # Run updates +``` + ## Development Workflow ### Project Structure + - **Modules** → `modules/custom/` - **Themes** → `themes/custom/` - **Configuration** → Export with `drush config:export` - **Profiles** → `profiles/custom/` ### Essential Development Commands + ```bash # Cache management drush cr # Clear all caches @@ -167,64 +912,61 @@ drush updatedb # Run database updates ### Debugging Tools #### Core Debugging & Information Commands -| Command | Purpose | Why it’s useful for debugging | -|----------------------------------|-------------------------------------------------------------------------|--------------------------------------------------------------------| -| `drush status` | Shows Drupal root, site path, database connection, Drush version, etc. | Quickly verify that Drush is pointing to the correct site and DB is connected. | -| `drush core-status` (D8+) | Same as above but more detailed in newer versions. | | -| `drush watchdog:show` / `drush ws` | Lists recent log messages (dblog entries). | Primary command to read the Drupal error/log messages without going to /admin/reports/dblog. Supports filters: `--severity=Error`, `--type=php`, etc. | -| `drush watchdog:delete all` | Clears the watchdog log. | Useful when logs become huge and slow down `ws`. | -| `drush sql:query "SELECT * FROM watchdog ORDER BY wid DESC LIMIT 50"` | Direct SQL access to logs when the database is very large. | Faster than `ws` on sites with millions of log entries. | + +| Command | Purpose | +| --- | --- | +| `drush status` | Shows Drupal root, site path, database connection, Drush version | +| `drush watchdog:show` / `drush ws` | Lists recent log messages. Filters: `--severity=Error`, `--type=php` | +| `drush watchdog:delete all` | Clears the watchdog log | +| `drush sql:query "SELECT * FROM watchdog ORDER BY wid DESC LIMIT 50"` | Direct SQL access to logs | #### Cache Debugging -| Command | Purpose | -|----------------------------------|-------------------------------------------------------------------------| -| `drush cache:rebuild` / `drush cr` | Rebuilds all caches (equivalent to “drush cc all” in D7). | Essential after any code or configuration change to ensure you’re not seeing cached behavior. | -| `drush cache:get :` | Retrieve a specific cache item (e.g., `drush cache:get config:core.extension`). | Verify whether a particular cache entry is present or corrupted. | -| `drush cache:clear ` | Clear only one cache bin (render, config, discovery, etc.). | + +| Command | Purpose | +| ---------------------------------- | ------------------------------ | +| `drush cache:rebuild` / `drush cr` | Rebuilds all caches | +| `drush cache:get :` | Retrieve a specific cache item | +| `drush cache:clear ` | Clear only one cache bin | #### Configuration Debugging -| Command | Purpose | -|----------------------------------------------|-------------------------------------------------------------------------| -| `drush config:get ` | Show a single configuration value (e.g., `drush config:get system.site`). | -| `drush config:set ` | Temporarily change a config value without using the UI. | -| `drush config:export` / `drush cex` | Export active config to sync directory. | -| `drush config:import` / `drush cim` | Import config – very useful to test if config issues cause errors. | -| `drush config:delete ` | Remove a config object (helps when orphaned config causes fatal errors).| + +| Command | Purpose | +| --------------------------------------- | -------------------------------------- | +| `drush config:get ` | Show a single configuration value | +| `drush config:set ` | Temporarily change a config value | +| `drush config:export` / `drush cex` | Export active config to sync directory | +| `drush config:import` / `drush cim` | Import config | +| `drush config:delete ` | Remove a config object | #### Module/Theming Debugging -| Command | Purpose | -|----------------------------------------|-------------------------------------------------------------------------| -| `drush pm:list --type=module --status=enabled` | List enabled modules. | -| `drush pm:enable ` / `drush en ` | Enable a module. | -| `drush pm:uninstall ` / `drush puninstall ` | Fully uninstall a module (removes config and data). | -| `drush pm:uninstall` without arguments → interactive mode is excellent for disabling suspected problematic modules quickly. | -| `drush theme:debug` (Drupal 9.4+) | Lists all theme suggestions for a given route or render array. | + +| Command | Purpose | +| ----------------------------------------------------------- | --------------------------- | +| `drush pm:list --type=module --status=enabled` | List enabled modules | +| `drush pm:enable ` / `drush en ` | Enable a module | +| `drush pm:uninstall ` / `drush puninstall ` | Fully uninstall a module | +| `drush theme:debug` | Lists all theme suggestions | #### Database & Entity Debugging -| Command | Purpose | -|----------------------------------------------|-------------------------------------------------------------------------| -| `drush sql:connect` | Outputs the CLI command to connect to the DB (useful for manual queries). | -| `drush sql:query` | Run arbitrary SQL. | -| `drush entity:info` | Show entity type definitions (useful when entity schema errors occur). | -| `drush php` | Opens an interactive PHP shell with Drupal bootstrapped (like `drush php:eval`). | -| `drush php:eval "code"` | Execute arbitrary PHP code in Drupal context (great for quick debugging). Example: `drush php:eval "dpm(\Drupal::state()->get('system.cron_last'));"` (with Devel) | - -#### Development & Error Reproduction -| Command | Purpose | -|----------------------------------------|-------------------------------------------------------------------------| -| `drush php:eval "var_dump(function_exists('my_problematic_function'));"` | Quick test if a function exists or what it returns. | -| `drush state:edit` / `drush state:get/set/delete` | Inspect or override Drupal state values (often used by broken modules).| -| `drush variable:get/set/delete` (D7 only) | Legacy equivalent of state commands. | -| `drush twig:debug` | Turn Twig debugging on/off and verify template suggestions. | -| `drush eval` (alias of php:eval) | Same as above. | + +| Command | Purpose | +| ----------------------- | ------------------------------------------------------- | +| `drush sql:connect` | Outputs the CLI command to connect to the DB | +| `drush sql:query` | Run arbitrary SQL | +| `drush entity:info` | Show entity type definitions | +| `drush php` | Opens an interactive PHP shell with Drupal bootstrapped | +| `drush php:eval "code"` | Execute arbitrary PHP code in Drupal context | #### Performance & Query Debugging -| Command | Purpose | -|----------------------------------------|-------------------------------------------------------------------------| -| `drush sql:query --db-prefix` | See queries with table prefixes expanded (helps reading raw SQL). | -| Enable Devel + `drush kint` or `dpm()` in code → instant output in terminal. | | + +```bash +drush sql:query --db-prefix # See queries with table prefixes expanded +drush twig:debug # Turn Twig debugging on/off +drush state:get/set/delete # Inspect or override Drupal state values +``` ### Performance Profiling + ```bash # Performance analysis drush cr # Rebuild caches @@ -232,137 +974,449 @@ drush sql:query "EXPLAIN ANALYZE SELECT ..." # Query analysis drush site:status # System status check # Use Webprofiler module for detailed profiling -# Access at http://127.0.0.1:8888/admin/config/development/devel/webprofiler +# Access at /admin/config/development/devel/webprofiler ``` ### Version Control Workflow + - **Commit messages**: Format `[#123456] Brief descriptive title` - **Branch from**: `develop` branch for features - **Atomic commits**: One logical change per commit - **Before pushing**: Run linting and tests -## Testing & Quality Assurance +## Advanced Development Patterns -### PHPUnit Testing Framework -Aim for ≥ 80% code coverage. Drupal provides multiple test types: +### Events & EventSubscribers -```bash -# Run all tests with coverage -vendor/bin/phpunit -v --coverage-html coverage/ +Prefer EventSubscribers over hooks for many use cases. They are more testable and follow Symfony conventions. -# Run specific test suites -vendor/bin/phpunit --testsuite unit # Unit tests (fast) -vendor/bin/phpunit --testsuite kernel # Kernel tests -vendor/bin/phpunit --testsuite functional # Functional tests (slower) -vendor/bin/phpunit --testsuite javascript # JavaScript tests +**EventSubscriber example**: -# Run specific tests -vendor/bin/phpunit --filter MyModuleUnitTest -vendor/bin/phpunit modules/custom/my_module/tests/src/Unit/ +```php +namespace Drupal\my_module\EventSubscriber; -# Run with custom configuration -SIMPLETEST_DB=sqlite://localhost/tmp.sqlite vendor/bin/phpunit +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Event\RequestEvent; + +class MyEventSubscriber implements EventSubscriberInterface { + + public static function getSubscribedEvents(): array { + return [ + KernelEvents::REQUEST => ['onKernelRequest', 100], + KernelEvents::RESPONSE => ['onKernelResponse'], + ]; + } + + public function onKernelRequest(RequestEvent $event): void { + // Act on every request. + $request = $event->getRequest(); + // ... + } + + public function onKernelResponse(\Symfony\Component\HttpKernel\Event\ResponseEvent $event): void { + // Modify response. + } +} ``` -### Test Types and Examples - -#### Unit Tests (fastest) -- **Purpose**: Test individual classes and methods in isolation -- **Base class**: Extend `UnitTestCase` from `Drupal\Tests\UnitTestCase` -- **Speed**: Fastest test type, no Drupal bootstrap required -- **Isolation**: Test one piece of functionality at a time -- **Dependencies**: Mock external dependencies and services -- **Location**: Place in `tests/src/Unit/` directory -- **Use cases**: Service logic calculations, utility functions, data transformations -- **Best practices**: Keep tests small, focused, and deterministic - -#### Kernel Tests (with database) -- **Purpose**: Test Drupal interactions with minimal Drupal environment -- **Base class**: Extend `KernelTestBase` from `Drupal\KernelTests\KernelTestBase` -- **Environment**: Partial Drupal bootstrap with in-memory database -- **Modules**: Declare required modules in `$modules` static property -- **Database**: Uses SQLite in-memory database for speed -- **Location**: Place in `tests/src/Kernel/` directory -- **Use cases**: Entity CRUD operations, configuration validation, service registration -- **Setup**: Install modules and configuration in `setUp()` method - -#### Functional Tests (with browser) -- **Purpose**: Test complete user interactions through browser simulation -- **Base class**: Extend `BrowserTestBase` from `Drupal\Tests\BrowserTestBase` -- **Environment**: Full Drupal bootstrap with real browser -- **Speed**: Slowest test type, full page loads required -- **Theme**: Set `$defaultTheme` property (usually 'stark' or 'claro') -- **Location**: Place in `tests/src/Functional/` directory -- **Use cases**: Form submissions, page access, user permissions, JavaScript interactions -- **Browser simulation**: Uses Goutte/ChromeDriver for browser automation -- **Assertions**: Use `$this->assertSession()` for web assertions +Register in `my_module.services.yml`: -### Code Quality Tools -```bash -# Static analysis (add to composer require) -vendor/bin/phpstan analyse # PHPStan analysis -vendor/bin/psalm # Psalm analysis +```yaml +my_module.event_subscriber: + class: Drupal\my_module\EventSubscriber\MyEventSubscriber + tags: + - { name: event_subscriber } +``` -# Security scanning -vendor/bin/drupal-check # Check for deprecated code -composer audit # Check for security advisories +**Drupal-specific events**: `HookEventDispatcher` module provides events for most Drupal hooks. Core events include entity events and kernel events. + +### Configuration Management + +**Config schema** (`config/schema/my_module.schema.yml`): + +```yaml +my_module.settings: + type: config_object + label: "My Module settings" + mapping: + api_key: + type: string + label: "API Key" + max_items: + type: integer + label: "Maximum items" + enabled_types: + type: sequence + label: "Enabled content types" + sequence: + type: string + label: "Content type" +``` -# Accessibility testing -vendor/bin/phpunit --group accessibility # Accessibility tests +**Config install** (`config/install/my_module.settings.yml`): + +```yaml +api_key: "" +max_items: 50 +enabled_types: + - article ``` -### JavaScript Testing +**Reading config**: + +```php +// In a service/controller (injected) +$value = $this->configFactory->get('my_module.settings')->get('api_key'); + +// In a .module file (less preferred) +$value = \Drupal::config('my_module.settings')->get('api_key'); +``` + +**Config workflow**: + ```bash -# Install JavaScript dependencies -npm install +# Export all configuration +drush config:export -# Run JavaScript tests -npm run test # Jest tests -npm run test:a11y # Accessibility tests +# Import configuration +drush config:import + +# View a single config value +drush config:get system.site + +# Edit config interactively +drush config:edit my_module.settings ``` -### Before Submitting Code +- **`config/install/`**: Required config installed when module is enabled +- **`config/optional/`**: Config installed only if dependencies are met +- **Config override**: Use `$config['system.performance']['css']['preprocess'] = FALSE;` in `settings.php` for environment-specific overrides +- **Config split**: Use `config_split` module for per-environment configuration (dev/staging/prod) + +### Batch API for Long Operations + +```php +use Drupal\Core\Batch\BatchBuilder; + +function my_module_process_items(array $items): void { + $batch = (new BatchBuilder()) + ->setTitle(t('Processing items')) + ->setInitMessage(t('Initializing...')) + ->setProgressMessage(t('Processed @current out of @total.')) + ->setErrorMessage(t('An error occurred during processing.')) + ->setFile(\Drupal::service('extension.list.module')->getPath('my_module') . '/my_module.batch.inc') + ->setFinishCallback('my_module_batch_finished') + ->addOperation('my_module_batch_process', [$items]); + + batch_set($batch->toArray()); +} + +// In my_module.batch.inc +function my_module_batch_process(array $items, array &$context): void { + if (!isset($context['sandbox']['progress'])) { + $context['sandbox']['progress'] = 0; + $context['sandbox']['max'] = count($items); + $context['sandbox']['items'] = $items; + } + + $limit = 10; + $items_to_process = array_slice($context['sandbox']['items'], $context['sandbox']['progress'], $limit); + + foreach ($items_to_process as $item) { + // Process each item. + $context['sandbox']['progress']++; + } + + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; + $context['message'] = t('Processed @progress of @max items', [ + '@progress' => $context['sandbox']['progress'], + '@max' => $context['sandbox']['max'], + ]); +} + +function my_module_batch_finished(bool $success, array $results, array $operations): void { + $messenger = \Drupal::messenger(); + if ($success) { + $messenger->addStatus(t('Batch completed successfully.')); + } + else { + $messenger->addError(t('An error occurred during batch processing.')); + } +} +``` + +- **Purpose**: Process large datasets without PHP timeout issues +- **Use cases**: Data migration, bulk updates, file processing, API calls +- **Memory management**: Processes data in chunks to prevent memory exhaustion + +### Queue API for Background Processing + +```php +namespace Drupal\my_module\Plugin\QueueWorker; + +use Drupal\Core\Queue\QueueWorkerBase; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Processes my module items. + * + * @QueueWorker( + * id = "my_module_processor", + * title = @Translation("My Module Processor"), + * cron = {"time" = 60} + * ) + */ +class MyQueueWorker extends QueueWorkerBase implements ContainerFactoryPluginInterface { + + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { + return new static($configuration, $plugin_id, $plugin_definition); + } + + public function processItem($data): void { + // Process the queue item. + if (!isset($data['type'])) { + throw new \InvalidArgumentException('Missing type in queue item.'); + } + // Do work... + } +} +``` + +**Adding items to the queue**: + +```php +\Drupal::queue('my_module_processor')->createItem(['type' => 'cleanup', 'node_id' => 123]); +``` + +- **Cron integration**: `cron = {"time" = 60}` processes items during cron for up to 60 seconds +- **Reliability**: Failed items are released back to the queue automatically +- **Logging**: Always log queue processing outcomes + +### AJAX Forms + +- **Trigger elements**: Add `#ajax` property to form elements (select, checkbox, button) +- **Callback method**: Reference callback method using `::methodName` syntax +- **Wrapper element**: Specify target element ID for AJAX response replacement +- **Response format**: Return form element or render array from callback +- **Event types**: Use 'change', 'click', 'blur' events as needed +- **Progress indicator**: Automatically shows loading indicator during AJAX requests +- **Error handling**: Implement try-catch blocks in AJAX callbacks +- **Form state**: Use `$form_state->getTriggeringElement()` to identify trigger +- **Multiple triggers**: Can have multiple AJAX elements in same form + +### Render API Deep Dive + +```php +// Full render array with all common properties +$build = [ + '#type' => 'container', + '#attributes' => ['class' => ['my-wrapper']], + 'heading' => [ + '#type' => 'html_tag', + '#tag' => 'h2', + '#value' => $this->t('My Heading'), + ], + 'content' => [ + '#theme' => 'item_list', + '#items' => $items, + '#empty' => $this->t('No items found.'), + ], + '#cache' => [ + 'keys' => ['my_module', 'list', $categoryId], + 'tags' => ['node_list', 'taxonomy_term:' . $categoryId], + 'contexts' => ['user.roles', 'languages:language_content'], + 'max-age' => 3600, + ], + '#attached' => [ + 'library' => ['my_module/my_module.styles'], + 'drupalSettings' => [ + 'my_module' => ['endpoint' => '/api/items'], + ], + ], + '#weight' => 10, +]; +``` + +- **`#pre_render` / `#post_render`**: Callbacks to modify render arrays before/after rendering +- **`#lazy_builder`**: Defers rendering of expensive content +- **`#create_placeholder`**: Generates a placeholder for BigPipe-style loading +- **`#attached`**: Attach CSS/JS libraries, settings, HTML head links, and HTTP headers + +### Migration API + +```yaml +# In migrations/my_migration.yml — source plugin +source: + plugin: csv + path: /path/to/data.csv + header_row_count: 1 + keys: + - id + column_names: + - id: [id, "Unique ID"] + - title: [title, "Title"] + +# Process plugin +process: + title: title + body/value: body + body/format: + plugin: default_value + default_value: basic_html + type: + plugin: default_value + default_value: article + uid: + plugin: default_value + default_value: 1 + +# Destination plugin +destination: + plugin: entity:node + default_bundle: article +``` + +**Custom process plugin**: + +```php +namespace Drupal\my_module\Plugin\migrate\process; + +use Drupal\migrate\ProcessPluginBase; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Row; + +/** + * Custom process plugin. + * + * @MigrateProcessPlugin( + * id = "my_custom_process" + * ) + */ +class MyCustomProcess extends ProcessPluginBase { + + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property): mixed { + // Transform the value during migration. + return strtoupper(trim($value)); + } +} +``` + +### Composer Management + ```bash -# Quality checklist -vendor/bin/phpcs --standard=Drupal . # Code style -vendor/bin/phpunit # Run tests -drush cr # Clear caches -drush updatedb # Run updates +# Add a module +composer require drupal/admin_toolbar + +# Add a module with a patch +composer require drupal/some_module +# Then add patch to composer.json extras: +# "patches": { +# "drupal/some_module": { +# "Fix description": "https://www.drupal.org/files/issues/2024-01-01/issue-12345-1.patch" +# } +# } + +# Update Drupal core +composer update drupal/core --with-all-dependencies + +# Run post-install steps +drush updatedb +drush config:import +drush cr ``` -## Pull Request Guidelines - -### Before Opening PR -1. **Create feature branch**: `git checkout -b feature/issue-123` -2. **Run quality checks**: - ```bash - drush cr - vendor/bin/phpcs --standard=Drupal . - vendor/bin/phpcs --standard=DrupalPractice . - vendor/bin/phpunit - drush updatedb - ``` -3. **Test manually**: Verify the feature works as expected -4. **Check for regressions**: Ensure no existing functionality is broken - -### PR Requirements -- **Title format**: `[#123456] Brief descriptive title` -- **Description must include**: - - Problem statement and solution approach - - Testing steps and verification methods - - Screenshots for UI changes - - Security considerations - - Accessibility impact - - Performance implications - -### Review Process -- **Automated checks**: Code style, tests, security scanning -- **Manual review**: Architecture, best practices, maintainability -- **Approval required**: At least one maintainer approval +**composer.json best practices**: + +- Use `drupal/core-recommended` for production, `drupal/core-dev` for development +- Pin major versions: `"drupal/core-recommended": "^11"` +- Use `composer-patches` plugin for community patches +- Commit `composer.lock` to version control +- Use `drupal.org` composer endpoint: `composer config repositories.drupal composer https://packages.drupal.org/8` + +### JavaScript & Frontend + +**Drupal behaviors** (not jQuery document.ready): + +```javascript +// js/my-module.js +(function (Drupal, drupalSettings) { + "use strict"; + + Drupal.behaviors.myModuleBehavior = { + attach: function (context, settings) { + // Run on every page load and AJAX response. + const elements = context.querySelectorAll(".my-element"); + elements.forEach(function (element) { + element.addEventListener("click", handleClick); + }); + }, + detach: function (context, settings, trigger) { + // Clean up when content is removed (AJAX, etc.). + const elements = context.querySelectorAll(".my-element"); + elements.forEach(function (element) { + element.removeEventListener("click", handleClick); + }); + }, + }; + + function handleClick(event) { + // Handle click. + } +})(Drupal, drupalSettings); +``` + +**Library definition** (`my_module.libraries.yml`): + +```yaml +my_module.styles: + version: VERSION + css: + component: + css/my-module.css: {} + js: + js/my-module.js: {} + dependencies: + - core/drupal + - core/drupalSettings +``` + +**Attaching libraries**: + +```php +// In render array +$build['#attached']['library'][] = 'my_module/my_module.styles'; + +// In twig +{{ + attach_library('my_module/my_module.styles') +}} +``` + +### Content Moderation & Workflows + +```php +// Workflows are typically configured via UI, but modules can interact: +use Drupal\workflows\Entity\Workflow; + +// Load a workflow +$workflow = Workflow::load('editorial'); + +// Check moderation state of a node +if ($node->hasField('moderation_state')) { + $state = $node->get('moderation_state')->value; +} + +// Transition a node to a new state +$node->set('moderation_state', 'published'); +$node->save(); +``` ## Troubleshooting Common Issues ### Installation Problems + ```bash # Composer memory issues php -d memory_limit=-1 /usr/local/bin/composer install @@ -372,24 +1426,25 @@ chmod 755 sites/default/files chmod 644 sites/default/settings.php chown -R www-data:www-data sites/default/files -# Database connection failed -# Check settings.php and database server status +# Database connection failed — check settings.php and database server status drush sql:connect # Test database connection # PHP extensions missing php -m # Check installed extensions -# Required: gd, xml, mbstring, json, pdo, curl ``` ### Performance Issues + ```bash # Identify slow queries drush sql:query "SELECT * FROM watchdog WHERE type = 'php' ORDER BY wid DESC LIMIT 10" # Check cache settings drush config:get system.performance +``` ### Module/Theme Development Issues + ```bash drush cr @@ -405,65 +1460,32 @@ drush watchdog:show --type=cron ``` ### Testing Issues + ```bash -# PHPUnit configuration problems -# Ensure phpunit.xml.dist exists and is configured +# PHPUnit configuration — ensure phpunit.xml.dist exists and is configured cp web/core/phpunit.xml.dist phpunit.xml -# Database setup for testing -# Edit phpunit.xml for SIMPLETEST_DB and SIMPLETEST_BASE_URL -SIMPLETEST_DB=mysql://root:password@localhost/drupal_test -SIMPLETEST_BASE_URL=http://127.0.0.1:8888 +# Database setup for testing — edit phpunit.xml for SIMPLETEST_DB and SIMPLETEST_BASE_URL +# SIMPLETEST_DB=mysql://root:password@localhost/drupal_test +# SIMPLETEST_BASE_URL=http://127.0.0.1:8888 -# Browser tests failing -# Install Selenium or ChromeDriver +# Browser tests failing — install Selenium or ChromeDriver # Ensure test environment variables are set ``` -## Advanced Development Patterns - -### Batch API for Long Operations -- **Purpose**: Process large datasets without PHP timeout issues -- **Use cases**: Data migration, bulk updates, file processing, API calls -- **Batch structure**: Create associative array with title, operations, and finished callback -- **Operations**: Array of callable methods and their arguments -- **Progress tracking**: Automatically shows progress bar to users -- **Error handling**: Implement proper exception handling in batch operations -- **User experience**: Provides real-time feedback during long operations -- **Memory management**: Processes data in chunks to prevent memory exhaustion - -### Queue API for Background Processing -- **Purpose**: Process tasks in the background without blocking user interaction -- **Queue creation**: Use `\Drupal::queue('queue_name')` to get queue instance -- **Item addition**: Use `createItem()` to add tasks to the queue -- **Processing**: Claim items with `claimItem()` and delete with `deleteItem()` -- **Cron integration**: Process queue items during cron runs for regular background tasks -- **Reliability**: Failed items can be released back to the queue -- **Worker plugins**: Create QueueWorker plugins for structured queue processing -- **Logging**: Implement proper logging for queue processing monitoring -- **Performance**: Process multiple items per cron run for efficiency - -### AJAX Forms -- **Trigger elements**: Add `#ajax` property to form elements (select, checkbox, button) -- **Callback method**: Reference callback method using `::methodName` syntax -- **Wrapper element**: Specify target element ID for AJAX response replacement -- **Response format**: Return form element or render array from callback -- **Event types**: Use 'change', 'click', 'blur' events as needed -- **Progress indicator**: Automatically shows loading indicator during AJAX requests -- **Error handling**: Implement try-catch blocks in AJAX callbacks -- **Form state**: Use `$form_state->getTriggeringElement()` to identify trigger -- **Multiple triggers**: Can have multiple AJAX elements in same form -- **Dynamic forms**: Update form options, show/hide fields based on user input - ## Additional Resources ### Official Documentation + - **Drupal API**: https://api.drupal.org - **Developer Guide**: https://www.drupal.org/docs/develop - **Coding Standards**: https://www.drupal.org/docs/develop/standards - **Security Best Practices**: https://www.drupal.org/docs/develop/security +- **Configuration Management**: https://www.drupal.org/docs/administering-a-drupal-site/configuration-management +- **Migration API**: https://www.drupal.org/docs/8/api/migrate-api ### Community Resources + - **DrupalAtYourFingertips**: https://www.drupalatyourfingertips.com - **Drupal Answers**: https://drupal.stackexchange.com - **Drupal.org**: https://www.drupal.org diff --git a/experimental/.kb/00-index.md b/experimental/.kb/00-index.md new file mode 100644 index 0000000..d4a03dd --- /dev/null +++ b/experimental/.kb/00-index.md @@ -0,0 +1,71 @@ +--- +title: Drupal Development Knowledge Base — Index +description: > + Master index for the Drupal AI Agent knowledge base. Read this file first to discover which files to load for your current task. Each file is self-contained with code examples, best practices, and cross-references to related topics. + + +tags: [index, overview, meta] +--- + +# Knowledge Base Index + +This knowledge base contains focused, self-contained guides for Drupal 10.x/11.x development. Each file covers one topic with concrete code examples. Files are numbered for discovery — read only what you need. + +## Quick Reference — When to Read What + +| You are working on... | Read this file | +| --------------------------------------- | ------------------------------------------------------ | +| Setting up a new module | [03-module-scaffolding.md](03-module-scaffolding.md) | +| Creating a service or using DI | [04-services-di.md](04-services-di.md) | +| Loading/querying entities | [05-entity-api.md](05-entity-api.md) | +| Building a plugin (block, field, etc.) | [06-plugins.md](06-plugins.md) | +| Implementing hooks | [07-hooks.md](07-hooks.md) | +| Building a form | [08-forms.md](08-forms.md) | +| Defining routes or controllers | [09-routes-controllers.md](09-routes-controllers.md) | +| Security concerns (XSS, CSRF, etc.) | [10-security.md](10-security.md) | +| Caching or performance | [11-caching-performance.md](11-caching-performance.md) | +| Want to know what NOT to do | [12-anti-patterns.md](12-anti-patterns.md) | +| Writing tests | [13-testing.md](13-testing.md) | +| Subscribing to events | [14-events.md](14-events.md) | +| Managing configuration | [15-configuration.md](15-configuration.md) | +| Batch or Queue processing | [16-batch-queue.md](16-batch-queue.md) | +| Render arrays, #attached, lazy builders | [17-render-api.md](17-render-api.md) | +| Data migration | [18-migration.md](18-migration.md) | +| Managing Composer dependencies | [19-composer.md](19-composer.md) | +| JavaScript or Drupal behaviors | [20-javascript.md](20-javascript.md) | +| Dev commands, debugging, Drush | [21-workflow.md](21-workflow.md) | +| Something is broken | [22-troubleshooting.md](22-troubleshooting.md) | + +## Always Read First + +- [01-project-overview.md](01-project-overview.md) — Tech stack, requirements, project conventions +- [02-code-standards.md](02-code-standards.md) — Coding standards and linting rules (must always be followed) + +## File Listing + +``` +.kb/ +├── 00-index.md ← You are here +├── 01-project-overview.md ← Tech stack, prerequisites +├── 02-code-standards.md ← PHP/YAML/Twig coding standards +├── 03-module-scaffolding.md ← Module file structure template +├── 04-services-di.md ← Services & dependency injection +├── 05-entity-api.md ← Entity loading, queries, creation +├── 06-plugins.md ← Plugin system (blocks, fields, etc.) +├── 07-hooks.md ← Hook implementations +├── 08-forms.md ← Forms API (simple, config, AJAX) +├── 09-routes-controllers.md ← Routes, controllers, access control +├── 10-security.md ← Security best practices +├── 11-caching-performance.md← Caching strategies & performance +├── 12-anti-patterns.md ← 14 "Never Do This" guidelines +├── 13-testing.md ← Unit, Kernel, Functional tests +├── 14-events.md ← EventSubscribers +├── 15-configuration.md ← Config schema, install, optional, split +├── 16-batch-queue.md ← Batch API & Queue API +├── 17-render-api.md ← Render arrays, #attached, lazy builders +├── 18-migration.md ← Migration API +├── 19-composer.md ← Composer management +├── 20-javascript.md ← Drupal behaviors, libraries +├── 21-workflow.md ← Dev commands, debugging, profiling +└── 22-troubleshooting.md ← Common issues and fixes +``` diff --git a/experimental/.kb/01-project-overview.md b/experimental/.kb/01-project-overview.md new file mode 100644 index 0000000..e89b848 --- /dev/null +++ b/experimental/.kb/01-project-overview.md @@ -0,0 +1,52 @@ +--- +title: Project Overview +description: > + Core technology stack, environment requirements, and project conventions for Drupal 10.x/11.x development. Read this file to understand the project's technical foundation. + + +tags: [overview, setup, prerequisites, stack] +--- + +# Project Overview + +## Technology Stack + +- **Core**: Drupal 10.x / 11.x — verify version with `composer show drupal/core` +- **PHP**: 8.3+ with extensions: gd, xml, mbstring, json, pdo, curl, zip +- **Database**: MySQL 8.0+ or PostgreSQL 12+ +- **Web Server**: Apache 2.4+ or Nginx 1.18+ +- **Package Manager**: Composer 2.0+ +- **CLI Tool**: Drush 13+ +- **Version Control**: Git + +## Key Components + +- Custom modules → `modules/custom/` (or `web/modules/custom/`) +- Custom themes → `themes/custom/` (or `web/themes/custom/`) +- Configuration → managed via Drush `config:export` / `config:import` +- Profiles → `profiles/custom/` +- Composer dependencies → managed via `composer.json` / `composer.lock` + +## Important Conventions + +- Always run commands from the **project root** unless specified otherwise +- Never commit database credentials — use environment variables or `settings.local.php` +- Follow Drupal coding standards — see [02-code-standards.md](02-code-standards.md) +- Use dependency injection — see [04-services-di.md](04-services-di.md) +- Always add cacheability metadata — see [11-caching-performance.md](11-caching-performance.md) + +## Verify Your Environment + +```bash +php -v # PHP 8.3+ +composer --version # Composer 2.0+ +drush --version # Drush 13+ +php -m # Check required extensions +drush status # Verify Drupal installation +``` + +## Related Files + +- [02-code-standards.md](02-code-standards.md) — Coding standards and linting +- [21-workflow.md](21-workflow.md) — Development commands and debugging +- [19-composer.md](19-composer.md) — Composer management diff --git a/experimental/.kb/02-code-standards.md b/experimental/.kb/02-code-standards.md new file mode 100644 index 0000000..2809103 --- /dev/null +++ b/experimental/.kb/02-code-standards.md @@ -0,0 +1,63 @@ +--- +title: Code Style and Standards +description: > + Drupal coding standards, linting rules, and code quality enforcement. These rules MUST be followed on every code change. Reject any code that fails Drupal Coder sniffs. + + +tags: [standards, php, yaml, twig, linting, phpcs, code-style] +--- + +# Code Style and Standards + +Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and PHPCS for enforcement. + +## PHP + +- **Indentation**: 2 spaces (no tabs) +- **Line length**: ≤ 80 characters +- **Naming**: CamelCase for classes/methods, snake_case for variables/functions +- **Braces**: Always use braces, even for single-line if/else +- **Returns**: Prefer early returns to reduce nesting +- **Docblocks**: Full PHPDoc blocks with `@param`, `@return`, `@throws` +- **Type hints**: Always use return type declarations and parameter types + +## YAML + +- 2-space indentation, lowercase keys +- Quote strings that contain special characters + +## Twig + +- Output: `{{ variable }}` +- Logic: `{% if condition %}{% endif %}` +- **Always escape** with `|e` filter (auto-escaping is on by default, but be explicit for safety) +- Never use `|raw` — see [12-anti-patterns.md](12-anti-patterns.md) + +## Linting Commands + +```bash +# Check code style +vendor/bin/phpcs --standard=Drupal --extensions=php,inc,module,install,info,yml src/ + +# Check best practices +vendor/bin/phpcs --standard=DrupalPractice --extensions=php,inc,module,install,info,yml src/ + +# Auto-fix style issues +vendor/bin/phpcs --standard=Drupal --fix src/ +``` + +## Static Analysis + +```bash +vendor/bin/phpstan analyse # PHPStan +vendor/bin/psalm # Psalm +vendor/bin/drupal-check # Check for deprecated code +composer audit # Security advisories +``` + +**Reject any code that fails Drupal Coder sniffs.** + +## Related Files + +- [12-anti-patterns.md](12-anti-patterns.md) — What NOT to do +- [13-testing.md](13-testing.md) — Testing standards diff --git a/experimental/.kb/03-module-scaffolding.md b/experimental/.kb/03-module-scaffolding.md new file mode 100644 index 0000000..9360673 --- /dev/null +++ b/experimental/.kb/03-module-scaffolding.md @@ -0,0 +1,94 @@ +--- +title: Module Scaffolding Template +description: > + Complete file structure and minimal starter files for creating a new Drupal custom module. Use this as a reference every time you create a new module. + + +tags: [module, scaffolding, template, structure, info-yml, composer] +--- + +# Module Scaffolding Template + +When creating a new custom module, follow this structure: + +``` +modules/custom/my_module/ +├── my_module.info.yml # Module metadata (required) +├── my_module.module # Hook implementations +├── my_module.routing.yml # Route definitions +├── my_module.services.yml # Service definitions +├── my_module.permissions.yml # Permission definitions +├── my_module.links.menu.yml # Menu links +├── my_module.links.action.yml # Action links +├── my_module.links.task.yml # Task (tab) links +├── my_module.libraries.yml # CSS/JS libraries +├── composer.json # PSR-4 autoloading +├── src/ +│ ├── Controller/ +│ ├── Form/ +│ ├── Plugin/ +│ │ ├── Block/ +│ │ ├── Field/ +│ │ │ ├── FieldFormatter/ +│ │ │ ├── FieldWidget/ +│ │ │ └── FieldType/ +│ │ └── QueueWorker/ +│ ├── EventSubscriber/ +│ ├── Access/ +│ ├── Entity/ +│ └── Service/ +├── config/ +│ ├── install/ # Config installed with module +│ ├── optional/ # Config if dependencies met +│ └── schema/ # Config schema +├── templates/ +├── css/ +├── js/ +└── tests/ + └── src/ + ├── Unit/ + ├── Kernel/ + └── Functional/ +``` + +## Minimal Required Files + +**my_module.info.yml**: + +```yaml +name: "My Module" +type: module +description: "Custom module description." +core_version_requirement: ^10 || ^11 +package: Custom +dependencies: + - drupal:node + - drupal:user +``` + +**composer.json** (PSR-4 autoloading for tests): + +```json +{ + "name": "drupal/my_module", + "type": "drupal-custom-module", + "description": "Custom module description.", + "autoload": { + "psr-4": { + "Drupal\\my_module\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Drupal\\Tests\\my_module\\": "tests/src/" + } + } +} +``` + +## Related Files + +- [04-services-di.md](04-services-di.md) — How to define services +- [07-hooks.md](07-hooks.md) — What goes in `.module` files +- [09-routes-controllers.md](09-routes-controllers.md) — Route definitions +- [15-configuration.md](15-configuration.md) — Config install/optional/schema diff --git a/experimental/.kb/04-services-di.md b/experimental/.kb/04-services-di.md new file mode 100644 index 0000000..13f7c1f --- /dev/null +++ b/experimental/.kb/04-services-di.md @@ -0,0 +1,114 @@ +--- +title: Services & Dependency Injection +description: > + How to define, register, and use Drupal services with dependency injection. Covers service definitions, constructor injection, ContainerFactoryPluginInterface, and core service discovery. ALWAYS prefer DI over static \Drupal:: calls. + + +tags: [services, dependency-injection, di, container, service-container] +--- + +# Services & Dependency Injection + +## Define a Service + +**my_module.services.yml**: + +```yaml +services: + my_module.my_service: + class: Drupal\my_module\Service\MyService + arguments: ["@entity_type.manager", "@logger.factory", "@config.factory"] + tags: + - { name: backend_overridable } +``` + +## Use Dependency Injection + +### In Controllers + +```php +namespace Drupal\my_module\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +class MyController extends ControllerBase { + + public function __construct( + protected EntityTypeManagerInterface $entityTypeManager, + ) {} + + public static function create(ContainerInterface $container): static { + return new static( + $container->get('entity_type.manager'), + ); + } + + public function content(): array { + $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple(); + return ['#theme' => 'item_list', '#items' => []]; + } +} +``` + +### In Plugins + +```php +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; + +class MyBlock extends BlockBase implements ContainerFactoryPluginInterface { + + public function __construct( + array $configuration, + string $plugin_id, + mixed $plugin_definition, + protected EntityTypeManagerInterface $entityTypeManager, + ) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + ); + } +} +``` + +## Common Core Services + +| Service ID | Purpose | +| ---------------------- | -------------------------- | +| `@entity_type.manager` | Entity loading and queries | +| `@config.factory` | Read/write configuration | +| `@logger.factory` | Logging (watchdog) | +| `@current_user` | Current user account | +| `@database` | Database connection | +| `@module_handler` | Module system | +| `@renderer` | Render API | +| `@string_translation` | Translation (`t()`) | +| `@messenger` | Status messages to user | +| `@request_stack` | HTTP request | +| `@state` | State API (transient data) | + +## Service Discovery + +```bash +drush php:eval "print_r(\Drupal::getContainer()->getServiceIds());" +``` + +## Rules + +- **ALWAYS** use dependency injection in services, controllers, and plugins +- **NEVER** use `\Drupal::` static calls in services/controllers/plugins — see [12-anti-patterns.md](12-anti-patterns.md) +- The only acceptable `\Drupal::` use is in `.module` hook functions — and even there, delegate to a service + +## Related Files + +- [05-entity-api.md](05-entity-api.md) — Using entity_type.manager service +- [06-plugins.md](06-plugins.md) — DI in plugins +- [12-anti-patterns.md](12-anti-patterns.md) — Why static calls are bad diff --git a/experimental/.kb/05-entity-api.md b/experimental/.kb/05-entity-api.md new file mode 100644 index 0000000..1adc349 --- /dev/null +++ b/experimental/.kb/05-entity-api.md @@ -0,0 +1,91 @@ +--- +title: Entity API & Queries +description: > + Loading, creating, querying, and accessing field values on Drupal entities. Covers EntityTypeManager, entity queries with accessCheck(TRUE), and field access patterns. + + +tags: [entity, node, entity-query, field-api, entity-type-manager] +--- + +# Entity API & Queries + +## Loading Entities + +```php +// Single entity +$node = \Drupal::entityTypeManager()->getStorage('node')->load(123); + +// Multiple entities +$nodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple([1, 2, 3]); + +// Load by properties +$nodes = \Drupal::entityTypeManager()->getStorage('node')->loadByProperties([ + 'type' => 'article', + 'status' => 1, +]); +``` + +## Entity Queries (always prefer over raw SQL) + +```php +// With injected service (preferred) +$ids = $this->entityTypeManager->getStorage('node')->getQuery() + ->condition('type', 'article') + ->condition('status', 1) + ->condition('field_category', $categoryId) + ->sort('created', 'DESC') + ->range(0, 10) + ->accessCheck(TRUE) // ALWAYS set explicitly in Drupal 10.2+ + ->execute(); + +$nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($ids); +``` + +> ⚠️ **`accessCheck(TRUE)` is required** on Drupal 10.2+. Omitting it throws deprecation warnings. See [12-anti-patterns.md](12-anti-patterns.md) #7. + +## Creating Entities + +```php +$node = \Drupal::entityTypeManager()->getStorage('node')->create([ + 'type' => 'article', + 'title' => 'My Article', + 'body' => [ + 'value' => 'Content here', + 'format' => 'full_html', + ], + 'status' => 1, + 'uid' => 1, +]); +$node->save(); +``` + +## Field Access + +```php +// ✅ Correct — explicit field access +$node->get('field_my_field')->value; +$node->get('field_my_field')->entity; // Entity reference +$node->get('field_my_field')->target_id; // Entity reference ID + +// ❌ Avoid — magic __get (works but less explicit) +$node->field_my_field->value; +``` + +## Common Operations + +```php +$node->label(); // Get title/label +$node->bundle(); // Get content type +$node->getEntityTypeId(); // Get entity type ('node', 'user', etc.) +$node->id(); // Get entity ID +$node->isPublished(); // Check published status +$node->set('title', 'New Title'); // Set a field value +$node->save(); // Save changes +$node->delete(); // Delete entity +``` + +## Related Files + +- [04-services-di.md](04-services-di.md) — Injecting entity_type.manager +- [11-caching-performance.md](11-caching-performance.md) — Cache tags for entities +- [12-anti-patterns.md](12-anti-patterns.md) — accessCheck, deprecated functions diff --git a/experimental/.kb/06-plugins.md b/experimental/.kb/06-plugins.md new file mode 100644 index 0000000..81caccf --- /dev/null +++ b/experimental/.kb/06-plugins.md @@ -0,0 +1,82 @@ +--- +title: Plugin System +description: > + Drupal's plugin system: annotation-based discovery, base classes, and the ContainerFactoryPluginInterface pattern. Includes a complete Block plugin example. + + +tags: [plugin, block, field-formatter, field-widget, queue-worker, annotation] +--- + +# Plugin System + +## Plugin Types + +Blocks, field formatters, field widgets, field types, menu links, QueueWorker, Condition, Action, and more. + +## Block Plugin Example (with DI) + +```php +namespace Drupal\my_module\Plugin\Block; + +use Drupal\Core\Block\BlockBase; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides a 'My Custom Block' block. + * + * @Block( + * id = "my_custom_block", + * admin_label = @Translation("My Custom Block"), + * category = @Translation("Custom"), + * context_definitions = { + * "node" = @ContextDefinition("entity:node", label = @Translation("Node")) + * } + * ) + */ +class MyCustomBlock extends BlockBase implements ContainerFactoryPluginInterface { + + public function __construct( + array $configuration, + string $plugin_id, + mixed $plugin_definition, + protected EntityTypeManagerInterface $entityTypeManager, + ) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + ); + } + + public function build(): array { + return [ + '#markup' => $this->t('Hello from my custom block!'), + '#cache' => [ + 'tags' => ['node_list'], + 'contexts' => ['user.roles'], + ], + ]; + } +} +``` + +## Key Concepts + +- **Discovery**: Annotation-based (as shown above) or YAML-based +- **Base classes**: Extend `BlockBase`, `FormatterBase`, `WidgetBase`, `QueueWorkerBase`, etc. +- **Placement**: `src/Plugin//` — e.g., `src/Plugin/Block/MyBlock.php` +- **Derivatives**: Use `DeriverBase` to create multiple plugins from one definition +- **DI**: Always implement `ContainerFactoryPluginInterface` when your plugin needs services + +## Related Files + +- [04-services-di.md](04-services-di.md) — Dependency injection patterns +- [16-batch-queue.md](16-batch-queue.md) — QueueWorker plugin type +- [06-plugins.md](06-plugins.md) — This file (self-reference) diff --git a/experimental/.kb/07-hooks.md b/experimental/.kb/07-hooks.md new file mode 100644 index 0000000..62e86f2 --- /dev/null +++ b/experimental/.kb/07-hooks.md @@ -0,0 +1,92 @@ +--- +title: Hooks +description: > + Drupal hook system: implementation patterns, common hooks with code examples, and best practices. Hooks live in modulename.module files — keep them thin and delegate complex logic to services. + + +tags: [hooks, hook-form-alter, hook-theme, hook-cron, hook-entity-presave] +--- + +# Hooks + +Hooks are implemented in `modulename.module` files. Keep them thin — delegate complex logic to services. + +## Common Hooks with Examples + +### hook_form_alter() + +```php +/** + * Implements hook_form_alter(). + */ +function my_module_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id): void { + if ($form_id === 'node_article_form') { + $form['title']['#title'] = t('Article Title'); + $form['actions']['submit']['#value'] = t('Publish Article'); + } +} +``` + +### hook_theme() + +```php +/** + * Implements hook_theme(). + */ +function my_module_theme($existing, $type, $theme, $path): array { + return [ + 'my_template' => [ + 'variables' => [ + 'title' => '', + 'items' => [], + ], + 'template' => 'my-template', + ], + ]; +} +``` + +### hook_entity_presave() + +```php +/** + * Implements hook_entity_presave(). + */ +function my_module_entity_presave(\Drupal\Core\Entity\EntityInterface $entity): void { + if ($entity->getEntityTypeId() === 'node' && $entity->bundle() === 'article') { + $entity->set('field_last_updated', \Drupal::time()->getRequestTime()); + } +} +``` + +### hook_cron() + +```php +/** + * Implements hook_cron(). + */ +function my_module_cron(): void { + \Drupal::queue('my_module_processor')->createItem(['type' => 'cleanup']); +} +``` + +## Key Points + +- **Naming**: Custom hooks follow `hook_modulename_action()` pattern +- **Type hints**: Always use type hints on parameters +- **Order**: Hooks fire in module weight order (lowest first) +- **Best practice**: Keep hooks focused — call services for complex logic. See [12-anti-patterns.md](12-anti-patterns.md) #14. + +## Other Common Hooks + +- `hook_menu_links_discovered_alter()` — Modify menu links +- `hook_theme_registry_alter()` — Modify theme hooks +- `hook_entity_delete()` — React to entity deletion +- `hook_user_insert()` / `hook_user_update()` — User lifecycle +- `hook_field_info()` — Define field types + +## Related Files + +- [04-services-di.md](04-services-di.md) — Where complex logic should live +- [08-forms.md](08-forms.md) — Form-related hooks +- [14-events.md](14-events.md) — EventSubscribers (alternative to many hooks) diff --git a/experimental/.kb/08-forms.md b/experimental/.kb/08-forms.md new file mode 100644 index 0000000..3fe12cc --- /dev/null +++ b/experimental/.kb/08-forms.md @@ -0,0 +1,146 @@ +--- +title: Forms API +description: > + Drupal Forms API: simple forms, configuration forms, validation, submission, and AJAX patterns. Includes complete copy-pasteable examples for FormBase and ConfigFormBase. + + +tags: [forms, form-api, config-form, ajax-form, validation] +--- + +# Forms API + +## Simple Form (FormBase) + +```php +namespace Drupal\my_module\Form; + +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; + +class CustomForm extends FormBase { + + public function getFormId(): string { + return 'my_module_custom_form'; + } + + public function buildForm(array $form, FormStateInterface $form_state): array { + $form['email'] = [ + '#type' => 'email', + '#title' => $this->t('Email Address'), + '#required' => TRUE, + ]; + + $form['category'] = [ + '#type' => 'select', + '#title' => $this->t('Category'), + '#options' => [ + 'news' => $this->t('News'), + 'events' => $this->t('Events'), + 'blog' => $this->t('Blog'), + ], + '#ajax' => [ + 'callback' => '::categoryChanged', + 'wrapper' => 'subcategory-wrapper', + 'event' => 'change', + ], + ]; + + $form['subcategory'] = [ + '#type' => 'container', + '#attributes' => ['id' => 'subcategory-wrapper'], + 'value' => [ + '#type' => 'textfield', + '#title' => $this->t('Subcategory'), + ], + ]; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + public function categoryChanged(array &$form, FormStateInterface $form_state): array { + return $form['subcategory']; + } + + public function validateForm(array &$form, FormStateInterface $form_state): void { + $email = $form_state->getValue('email'); + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + $form_state->setErrorByName('email', $this->t('Please enter a valid email address.')); + } + } + + public function submitForm(array &$form, FormStateInterface $form_state): void { + $this->messenger()->addStatus($this->t('Form submitted successfully.')); + $form_state->setRedirect(''); + } +} +``` + +## Configuration Form (ConfigFormBase) + +```php +namespace Drupal\my_module\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; + +class SettingsForm extends ConfigFormBase { + + public function getFormId(): string { + return 'my_module_settings'; + } + + protected function getEditableConfigNames(): array { + return ['my_module.settings']; + } + + public function buildForm(array $form, FormStateInterface $form_state): array { + $config = $this->config('my_module.settings'); + + $form['api_key'] = [ + '#type' => 'textfield', + '#title' => $this->t('API Key'), + '#default_value' => $config->get('api_key'), + '#required' => TRUE, + ]; + + $form['max_items'] = [ + '#type' => 'number', + '#title' => $this->t('Maximum Items'), + '#default_value' => $config->get('max_items') ?? 50, + '#min' => 1, + '#max' => 500, + ]; + + return parent::buildForm($form, $form_state); + } + + public function submitForm(array &$form, FormStateInterface $form_state): void { + $this->config('my_module.settings') + ->set('api_key', $form_state->getValue('api_key')) + ->set('max_items', $form_state->getValue('max_items')) + ->save(); + + parent::submitForm($form, $form_state); + } +} +``` + +## AJAX Forms Quick Reference + +- Add `#ajax` property to any form element +- Callback: `'::methodName'` syntax +- Wrapper: target element ID for replacement +- Events: `'change'`, `'click'`, `'blur'` +- Trigger: `$form_state->getTriggeringElement()` +- Error handling: try-catch in callbacks + +## Related Files + +- [09-routes-controllers.md](09-routes-controllers.md) — Routing forms to paths +- [15-configuration.md](15-configuration.md) — Config storage details +- [17-render-api.md](17-render-api.md) — Render arrays used in forms diff --git a/experimental/.kb/09-routes-controllers.md b/experimental/.kb/09-routes-controllers.md new file mode 100644 index 0000000..91017c6 --- /dev/null +++ b/experimental/.kb/09-routes-controllers.md @@ -0,0 +1,128 @@ +--- +title: Routes & Controllers +description: > + Drupal routing system, controller classes, route parameters, and custom access checkers. Includes complete YAML route definitions and PHP examples. + + +tags: [routing, controllers, routes, access-checker, permissions] +--- + +# Routes & Controllers + +## Route Definitions (my_module.routing.yml) + +```yaml +# Basic controller route with parameter upcasting +my_module.content: + path: "/my-module/{node}" + defaults: + _controller: '\Drupal\my_module\Controller\MyController::content' + _title: "My Module Page" + requirements: + _permission: "access content" + node: \d+ + +# Form route +my_module.settings: + path: "/admin/config/my-module/settings" + defaults: + _form: '\Drupal\my_module\Form\SettingsForm' + _title: "My Module Settings" + requirements: + _permission: "administer site configuration" + +# Route with custom access checker +my_module.custom_access: + path: "/my-module/custom/{node}" + defaults: + _controller: '\Drupal\my_module\Controller\MyController::customPage' + _title_callback: '\Drupal\my_module\Controller\MyController::pageTitle' + requirements: + _custom_access: '\Drupal\my_module\Access\MyAccessChecker::access' +``` + +## Controller with Dependency Injection + +```php +namespace Drupal\my_module\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\node\NodeInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +class MyController extends ControllerBase { + + public function __construct( + protected EntityTypeManagerInterface $entityTypeManager, + ) {} + + public static function create(ContainerInterface $container): static { + return new static( + $container->get('entity_type.manager'), + ); + } + + public function content(NodeInterface $node): array { + return [ + '#theme' => 'my_template', + '#title' => $node->label(), + '#items' => $this->getItems($node), + '#cache' => [ + 'tags' => ['node:' . $node->id()], + 'contexts' => ['user.permissions'], + ], + '#attached' => [ + 'library' => ['my_module/my_module.styles'], + ], + ]; + } + + public function pageTitle(NodeInterface $node): string { + return $this->t('Page: @title', ['@title' => $node->label()]); + } +} +``` + +## Custom Access Checker + +```php +namespace Drupal\my_module\Access; + +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Routing\Access\AccessInterface; +use Drupal\node\NodeInterface; + +class MyAccessChecker implements AccessInterface { + + public function access(NodeInterface $node): AccessResult { + return AccessResult::allowedIf($node->isPublished()) + ->addCacheTags(['node:' . $node->id()]) + ->cachePerUser(); + } +} +``` + +Register as a service with the `access_check` tag: + +```yaml +# my_module.services.yml +my_module.access_checker: + class: Drupal\my_module\Access\MyAccessChecker + tags: + - { name: access_check } +``` + +## Access Control Options + +- `_permission: 'permission name'` — Permission-based +- `_role: 'role_name'` — Role-based +- `_access: 'TRUE'` — Public route +- `_custom_access: '::method'` — Custom logic +- `_entity_access: 'node.view'` — Entity-level access + +## Related Files + +- [04-services-di.md](04-services-di.md) — DI in controllers +- [10-security.md](10-security.md) — Security best practices +- [08-forms.md](08-forms.md) — Routing forms diff --git a/experimental/.kb/10-security.md b/experimental/.kb/10-security.md new file mode 100644 index 0000000..16d8e05 --- /dev/null +++ b/experimental/.kb/10-security.md @@ -0,0 +1,77 @@ +--- +title: Security Best Practices +description: > + Drupal security requirements: input sanitization, XSS prevention, CSRF protection, SQL injection avoidance, and secure coding patterns. + + +tags: [security, xss, csrf, sql-injection, sanitization, permissions] +--- + +# Security Best Practices + +## Input Sanitization + +- **Render arrays**: Use `#plain_text` for untrusted content +- **Twig**: Always use `|e` filter (or rely on auto-escaping) +- **Never** use `#markup` with unsanitized user input +- **Never** use `|raw` in Twig + +## CSRF Protection + +- Forms with side effects automatically include CSRF tokens +- For custom forms, ensure `#token` is set + +## SQL Injection + +- **Always** use Entity Query — see [05-entity-api.md](05-entity-api.md) +- If you must use raw SQL, use parameterized queries: + + ```php + $result = $this->database->query( + "SELECT * FROM {node} WHERE type = :type", + [':type' => $type] + ); + ``` + +## XSS Prevention + +```php +// ✅ Correct — safe +$build['output'] = ['#plain_text' => $user_input]; + +// ❌ Wrong — XSS vulnerability +$build['output'] = ['#markup' => $user_input]; +``` + +In Twig: + +```twig +{# ✅ Correct — auto-escaped #} +{{ user_input }} + +{# ❌ Never do this #} +{{ user_input|raw }} +``` + +## Access Control + +- Always set `_permission`, `_role`, or `_custom_access` on routes — see [09-routes-controllers.md](09-routes-controllers.md) +- Always use `->accessCheck(TRUE)` on entity queries — see [05-entity-api.md](05-entity-api.md) +- Check entity access: `$entity->access('view')`, `$entity->access('update')` + +## File Uploads + +- Validate file types and sizes via Drupal's file API +- Never trust MIME types from the client + +## Credentials + +- Never commit `settings.php` with credentials +- Use environment variables or `settings.local.php` (excluded from VCS) +- Never hardcode API keys — use Drupal's config or key module + +## Related Files + +- [12-anti-patterns.md](12-anti-patterns.md) — Security anti-patterns +- [09-routes-controllers.md](09-routes-controllers.md) — Route access control +- [05-entity-api.md](05-entity-api.md) — Safe entity queries diff --git a/experimental/.kb/11-caching-performance.md b/experimental/.kb/11-caching-performance.md new file mode 100644 index 0000000..ac56302 --- /dev/null +++ b/experimental/.kb/11-caching-performance.md @@ -0,0 +1,98 @@ +--- +title: Caching & Performance +description: > + Drupal caching strategies: render cache, cache tags, contexts, max-age, lazy builders, placeholder strategy, and Redis/Memcache. Every render array that depends on data MUST specify cache metadata. + + +tags: [cache, performance, render-cache, cache-tags, lazy-builder, redis] +--- + +# Caching & Performance + +## Cache Metadata (Required on Every Render Array) + +```php +$build = [ + '#theme' => 'item_list', + '#items' => $items, + '#cache' => [ + 'keys' => ['my_module:item_list:' . $categoryId], + 'tags' => ['node_list', 'taxonomy_term:' . $categoryId], + 'contexts' => ['user.roles'], + 'max-age' => 3600, + ], +]; +``` + +## Cache Tags + +Invalidate when the underlying data changes: + +```php +// Entity-specific +['node:123', 'node:456'] + +// List-level (invalidated when ANY node changes) +['node_list'] + +// Config +['config:system.site'] +``` + +## Cache Contexts + +Vary output by: + +```php +['user.roles'] // Different per role +['user.permissions'] // Different per permission set +['languages:language_content'] // Different per language +['url'] // Different per URL +['ip'] // Different per IP +``` + +## Lazy Builders (for expensive operations) + +```php +$build['expensive'] = [ + '#lazy_builder' => [ + '\Drupal\my_module\Service\MyLazyBuilder::renderExpensiveContent', + [$param1, $param2], + ], + '#create_placeholder' => TRUE, +]; +``` + +## Caching Strategies + +| Strategy | Use Case | +| ----------------------- | --------------------------------------- | +| **Render cache** | Cache complex markup with tags/contexts | +| **Dynamic page cache** | Auto-cached for anonymous users | +| **Internal page cache** | Full page cache for anonymous | +| **Entity cache** | Automatic — invalidate via cache tags | +| **Redis/Memcache** | Production distributed caching | + +## Performance Rules + +- Always add `#cache` to render arrays that depend on data +- Use `loadMultiple()` instead of individual `load()` calls +- Use entity queries instead of raw SQL — see [05-entity-api.md](05-entity-api.md) +- Profile before optimizing — identify actual bottlenecks + +## Server Tuning + +```bash +# php.ini +memory_limit = 256M +max_execution_time = 300 + +# my.cnf +innodb_buffer_pool_size = 1G +``` + +## Related Files + +- [17-render-api.md](17-render-api.md) — Full render array reference +- [05-entity-api.md](05-entity-api.md) — Entity queries +- [12-anti-patterns.md](12-anti-patterns.md) — Missing cache metadata (#10) diff --git a/experimental/.kb/12-anti-patterns.md b/experimental/.kb/12-anti-patterns.md new file mode 100644 index 0000000..74fac92 --- /dev/null +++ b/experimental/.kb/12-anti-patterns.md @@ -0,0 +1,38 @@ +--- +title: Anti-Patterns — Never Do This +description: > + 14 critical Drupal development mistakes that AI agents must avoid. Every item on this list is a common error that leads to bugs, security vulnerabilities, or maintenance nightmares. + + +tags: [anti-patterns, best-practices, never-do-this, security, code-quality] +--- + +# Anti-Patterns — Never Do This + +1. **Never use `\Drupal::` static calls in services, controllers, or plugins** — Use dependency injection. The only acceptable use is in `.module` hook functions. See [04-services-di.md](04-services-di.md). + +2. **Never query the database directly when Entity Query suffices** — Use `$this->entityTypeManager->getStorage('node')->getQuery()`. See [05-entity-api.md](05-entity-api.md). + +3. **Never use `|raw` in Twig** — Use `|e` or rely on auto-escaping. If you need raw HTML, use `#type => 'processed_text'` or `check_markup()`. See [10-security.md](10-security.md). + +4. **Never create monolithic `hook_form_alter()` functions** — If the alter logic is complex, delegate to a service. + +5. **Never store configuration in state that belongs in config** — State (`\Drupal::state()`) = ephemeral data (last cron, temp flags). Config (`\Drupal::configFactory()`) = structured, exportable settings. See [15-configuration.md](15-configuration.md). + +6. **Never use `#markup` with unsanitized user input** — Always use `#plain_text` or `Html::escape()`. See [10-security.md](10-security.md). + +7. **Never skip `accessCheck(TRUE)` on entity queries** — Required since Drupal 10.2. Omission throws deprecation warnings, will be fatal in Drupal 12. See [05-entity-api.md](05-entity-api.md). + +8. **Never hardcode entity IDs, user IDs, or paths** — Use configuration, route names, and dynamic lookups. + +9. **Never use `hook_views_data()` without proper table aliases** — Always prefix columns to avoid SQL ambiguity. + +10. **Never ignore cacheability metadata** — Every render array that depends on data must specify `#cache` tags, contexts, and max-age. See [11-caching-performance.md](11-caching-performance.md). + +11. **Never commit `settings.php` with credentials** — Use environment variables or `settings.local.php`. See [10-security.md](10-security.md). + +12. **Never use `node_load()` or other deprecated procedural functions** — Use `\Drupal::entityTypeManager()->getStorage('node')->load()`. See [05-entity-api.md](05-entity-api.md). + +13. **Never use global variables like `$_GET`, `$_POST`, `$_SERVER`** — Use Symfony's `Request` object via dependency injection. + +14. **Never put business logic in `.module` files** — Delegate to services. The `.module` file should be thin: hooks that call services. diff --git a/experimental/.kb/13-testing.md b/experimental/.kb/13-testing.md new file mode 100644 index 0000000..8a08925 --- /dev/null +++ b/experimental/.kb/13-testing.md @@ -0,0 +1,141 @@ +--- +title: Testing & Quality Assurance +description: > + Drupal testing with PHPUnit: Unit, Kernel, and Functional test examples. Covers test base classes, configuration, and code quality tools. + + +tags: [testing, phpunit, unit-test, kernel-test, functional-test, quality] +--- + +# Testing & Quality Assurance + +## Running Tests + +```bash +# All tests +vendor/bin/phpunit -v --coverage-html coverage/ + +# By suite +vendor/bin/phpunit --testsuite unit # Fast, no Drupal +vendor/bin/phpunit --testsuite kernel # DB + minimal Drupal +vendor/bin/phpunit --testsuite functional # Full browser +vendor/bin/phpunit --testsuite javascript # JS tests + +# Specific test +vendor/bin/phpunit --filter MyModuleUnitTest +vendor/bin/phpunit modules/custom/my_module/tests/src/Unit/ +``` + +## Unit Test (fastest — no Drupal bootstrap) + +```php +namespace Drupal\Tests\my_module\Unit; + +use Drupal\Tests\UnitTestCase; +use Drupal\my_module\Service\MyService; +use Prophecy\PhpUnit\ProphecyTrait; + +class MyServiceTest extends UnitTestCase { + use ProphecyTrait; + + public function testProcessReturnsExpectedValue(): void { + $logger = $this->prophesize('\Psr\Log\LoggerInterface'); + $service = new MyService($logger->reveal()); + $result = $service->process('input'); + $this->assertEquals('expected_output', $result); + } + + public function testProcessThrowsOnEmptyInput(): void { + $logger = $this->prophesize('\Psr\Log\LoggerInterface'); + $service = new MyService($logger->reveal()); + $this->expectException(\InvalidArgumentException::class); + $service->process(''); + } +} +``` + +- **Location**: `tests/src/Unit/` +- **Base class**: `Drupal\Tests\UnitTestCase` +- **Dependencies**: Mock with Prophecy + +## Kernel Test (partial Drupal + in-memory DB) + +```php +namespace Drupal\Tests\my_module\Kernel; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\node\Entity\Node; +use Drupal\node\Entity\NodeType; + +class MyModuleKernelTest extends KernelTestBase { + + protected static $modules = ['system', 'node', 'user', 'my_module']; + + protected function setUp(): void { + parent::setUp(); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installConfig(['my_module']); + NodeType::create(['type' => 'article', 'name' => 'Article'])->save(); + } + + public function testNodeCreation(): void { + $node = Node::create([ + 'type' => 'article', + 'title' => 'Test Article', + 'status' => 1, + ]); + $node->save(); + $this->assertNotNull($node->id()); + $this->assertEquals('article', $node->bundle()); + } +} +``` + +- **Location**: `tests/src/Kernel/` +- **Base class**: `Drupal\KernelTests\KernelTestBase` + +## Functional Test (full browser simulation) + +```php +namespace Drupal\Tests\my_module\Functional; + +use Drupal\Tests\BrowserTestBase; + +class MyModuleFunctionalTest extends BrowserTestBase { + + protected $defaultTheme = 'stark'; + protected static $modules = ['node', 'my_module']; + + protected function setUp(): void { + parent::setUp(); + $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + $user = $this->drupalCreateUser(['access content', 'create article content']); + $this->drupalLogin($user); + } + + public function testArticleCreation(): void { + $this->drupalGet('/node/add/article'); + $this->assertSession()->statusCodeEquals(200); + $this->submitForm(['title[0][value]' => 'Test Article'], 'Save'); + $this->assertSession()->pageTextContains('has been created.'); + } +} +``` + +- **Location**: `tests/src/Functional/` +- **Base class**: `Drupal\Tests\BrowserTestBase` + +## Quality Tools + +```bash +vendor/bin/phpstan analyse # Static analysis +vendor/bin/drupal-check # Deprecated code check +composer audit # Security advisories +vendor/bin/phpcs --standard=Drupal . # Code style +``` + +## Related Files + +- [02-code-standards.md](02-code-standards.md) — Code style rules +- [03-module-scaffolding.md](03-module-scaffolding.md) — Test directory structure diff --git a/experimental/.kb/14-events.md b/experimental/.kb/14-events.md new file mode 100644 index 0000000..a90d60d --- /dev/null +++ b/experimental/.kb/14-events.md @@ -0,0 +1,73 @@ +--- +title: Events & EventSubscribers +description: > + Symfony EventDispatcher in Drupal: replacing hooks with event subscribers for better testability. Includes complete example with service registration. + + +tags: [events, event-subscriber, symfony, kernel-events] +--- + +# Events & EventSubscribers + +Prefer EventSubscribers over hooks for many use cases. They are more testable and follow Symfony conventions. + +## EventSubscriber Example + +```php +namespace Drupal\my_module\EventSubscriber; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Event\RequestEvent; + +class MyEventSubscriber implements EventSubscriberInterface { + + public static function getSubscribedEvents(): array { + return [ + KernelEvents::REQUEST => ['onKernelRequest', 100], + KernelEvents::RESPONSE => ['onKernelResponse'], + ]; + } + + public function onKernelRequest(RequestEvent $event): void { + $request = $event->getRequest(); + // Act on every request. + } + + public function onKernelResponse(\Symfony\Component\HttpKernel\Event\ResponseEvent $event): void { + // Modify response. + } +} +``` + +## Register as a Service + +```yaml +# my_module.services.yml +my_module.event_subscriber: + class: Drupal\my_module\EventSubscriber\MyEventSubscriber + arguments: ["@current_user", "@config.factory"] + tags: + - { name: event_subscriber } +``` + +The `event_subscriber` tag is required — Drupal auto-discovers subscribers via this tag. + +## Common Kernel Events + +| Event | When | +| -------------------------- | --------------------------------- | +| `KernelEvents::REQUEST` | Incoming request | +| `KernelEvents::RESPONSE` | Outgoing response | +| `KernelEvents::EXCEPTION` | Uncaught exception | +| `KernelEvents::VIEW` | Controller returns non-Response | +| `KernelEvents::CONTROLLER` | Controller found but not executed | + +## Drupal-Specific Events + +The `HookEventDispatcher` contrib module provides events for most Drupal hooks (entity presave, form alter, etc.). Core also dispatches events for entity operations. + +## Related Files + +- [07-hooks.md](07-hooks.md) — Traditional hooks (alternative approach) +- [04-services-di.md](04-services-di.md) — Service definitions diff --git a/experimental/.kb/15-configuration.md b/experimental/.kb/15-configuration.md new file mode 100644 index 0000000..3663e55 --- /dev/null +++ b/experimental/.kb/15-configuration.md @@ -0,0 +1,95 @@ +--- +title: Configuration Management +description: > + Drupal's Configuration API: schema definition, install vs optional config, reading/writing config, config split for per-environment settings, and Drush config workflow commands. + + +tags: [configuration, config, config-schema, config-split, drush-cex] +--- + +# Configuration Management + +## Config Schema (config/schema/my_module.schema.yml) + +```yaml +my_module.settings: + type: config_object + label: "My Module settings" + mapping: + api_key: + type: string + label: "API Key" + max_items: + type: integer + label: "Maximum items" + enabled_types: + type: sequence + label: "Enabled content types" + sequence: + type: string + label: "Content type" +``` + +## Install vs Optional Config + +- **`config/install/`** — Installed when module is enabled (required) + + ```yaml + # config/install/my_module.settings.yml + api_key: "" + max_items: 50 + enabled_types: + - article + ``` + +- **`config/optional/`** — Installed only if dependencies are met (e.g., a field config that requires a content type from another module) + +## Reading Config + +```php +// In a service/controller (injected — preferred) +$value = $this->configFactory->get('my_module.settings')->get('api_key'); + +// In a .module file (less preferred) +$value = \Drupal::config('my_module.settings')->get('api_key'); +``` + +## Writing Config + +```php +$this->configFactory->getEditable('my_module.settings') + ->set('api_key', 'new-value') + ->save(); +``` + +## Config Override (settings.php) + +```php +// Environment-specific overrides (not exported) +$config['system.performance']['css']['preprocess'] = FALSE; +$config['system.performance']['js']['preprocess'] = FALSE; +``` + +## Drush Config Workflow + +```bash +drush config:export # Export active config to sync directory +drush config:import # Import from sync directory +drush config:get # Show config value +drush config:set # Change config +drush config:edit # Edit in editor +drush config:delete # Remove config object +``` + +## Config Split (per-environment) + +Use the `config_split` module to manage different configurations for dev/staging/prod: + +- Dev: disable CSS aggregation, enable Devel +- Staging: disable CSS aggregation, disable Devel +- Prod: enable everything + +## Related Files + +- [08-forms.md](08-forms.md) — ConfigFormBase for config UIs +- [15-configuration.md](15-configuration.md) — This file (self-reference) diff --git a/experimental/.kb/16-batch-queue.md b/experimental/.kb/16-batch-queue.md new file mode 100644 index 0000000..e45eb23 --- /dev/null +++ b/experimental/.kb/16-batch-queue.md @@ -0,0 +1,114 @@ +--- +title: Batch API & Queue API +description: > + Processing large datasets and background tasks in Drupal. Batch API for user-facing long operations with progress bars. Queue API for cron-based background processing with QueueWorker plugins. + + +tags: [batch, queue, queue-worker, cron, background-processing] +--- + +# Batch API & Queue API + +## Batch API (user-facing long operations) + +```php +use Drupal\Core\Batch\BatchBuilder; + +function my_module_process_items(array $items): void { + $batch = (new BatchBuilder()) + ->setTitle(t('Processing items')) + ->setInitMessage(t('Initializing...')) + ->setProgressMessage(t('Processed @current out of @total.')) + ->setErrorMessage(t('An error occurred during processing.')) + ->setFile(\Drupal::service('extension.list.module')->getPath('my_module') . '/my_module.batch.inc') + ->setFinishCallback('my_module_batch_finished') + ->addOperation('my_module_batch_process', [$items]); + + batch_set($batch->toArray()); +} +``` + +```php +// my_module.batch.inc +function my_module_batch_process(array $items, array &$context): void { + if (!isset($context['sandbox']['progress'])) { + $context['sandbox']['progress'] = 0; + $context['sandbox']['max'] = count($items); + $context['sandbox']['items'] = $items; + } + + $limit = 10; + $items_to_process = array_slice($context['sandbox']['items'], $context['sandbox']['progress'], $limit); + + foreach ($items_to_process as $item) { + // Process each item. + $context['sandbox']['progress']++; + } + + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; + $context['message'] = t('Processed @progress of @max', [ + '@progress' => $context['sandbox']['progress'], + '@max' => $context['sandbox']['max'], + ]); +} + +function my_module_batch_finished(bool $success, array $results, array $operations): void { + $messenger = \Drupal::messenger(); + if ($success) { + $messenger->addStatus(t('Batch completed successfully.')); + } + else { + $messenger->addError(t('An error occurred during batch processing.')); + } +} +``` + +**Use cases**: Data migration, bulk updates, file processing, API calls. + +## Queue API (background processing) + +### QueueWorker Plugin + +```php +namespace Drupal\my_module\Plugin\QueueWorker; + +use Drupal\Core\Queue\QueueWorkerBase; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * @QueueWorker( + * id = "my_module_processor", + * title = @Translation("My Module Processor"), + * cron = {"time" = 60} + * ) + */ +class MyQueueWorker extends QueueWorkerBase implements ContainerFactoryPluginInterface { + + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { + return new static($configuration, $plugin_id, $plugin_definition); + } + + public function processItem($data): void { + if (!isset($data['type'])) { + throw new \InvalidArgumentException('Missing type in queue item.'); + } + // Process the item... + } +} +``` + +### Adding Items to Queue + +```php +\Drupal::queue('my_module_processor')->createItem(['type' => 'cleanup', 'node_id' => 123]); +``` + +- `cron = {"time" = 60}` — processes items during cron for up to 60 seconds +- Failed items are released back to the queue automatically +- Always log queue processing outcomes + +## Related Files + +- [06-plugins.md](06-plugins.md) — Plugin system +- [07-hooks.md](07-hooks.md) — hook_cron() for triggering queues diff --git a/experimental/.kb/17-render-api.md b/experimental/.kb/17-render-api.md new file mode 100644 index 0000000..606ad96 --- /dev/null +++ b/experimental/.kb/17-render-api.md @@ -0,0 +1,93 @@ +--- +title: Render API Deep Dive +description: > + Drupal's Render API: render arrays, #cache, #attached, #lazy_builder, #create_placeholder, #pre_render, #post_render, and render element types. + + +tags: [render, render-array, attached, lazy-builder, placeholder, theme] +--- + +# Render API + +## Complete Render Array Example + +```php +$build = [ + '#type' => 'container', + '#attributes' => ['class' => ['my-wrapper']], + 'heading' => [ + '#type' => 'html_tag', + '#tag' => 'h2', + '#value' => $this->t('My Heading'), + ], + 'content' => [ + '#theme' => 'item_list', + '#items' => $items, + '#empty' => $this->t('No items found.'), + ], + '#cache' => [ + 'keys' => ['my_module', 'list', $categoryId], + 'tags' => ['node_list', 'taxonomy_term:' . $categoryId], + 'contexts' => ['user.roles', 'languages:language_content'], + 'max-age' => 3600, + ], + '#attached' => [ + 'library' => ['my_module/my_module.styles'], + 'drupalSettings' => [ + 'my_module' => ['endpoint' => '/api/items'], + ], + ], + '#weight' => 10, +]; +``` + +## Key Properties + +| Property | Purpose | +| --------------------- | ---------------------------------------------------------------- | +| `#type` | Render element type (`container`, `html_tag`, `item_list`, etc.) | +| `#theme` | Theme hook to use for rendering | +| `#markup` | Raw HTML (trusted only!) | +| `#plain_text` | Auto-escaped text output | +| `#cache` | Cache metadata (keys, tags, contexts, max-age) | +| `#attached` | Libraries, settings, HTTP headers | +| `#weight` | Sort order | +| `#attributes` | HTML attributes (class, id, data-\*) | +| `#access` | Boolean access check | +| `#lazy_builder` | Deferred rendering callback | +| `#create_placeholder` | Generate BigPipe placeholder | +| `#pre_render` | Callbacks to modify before rendering | +| `#post_render` | Callbacks to modify after rendering | + +## #attached — Libraries & Settings + +```php +$build['#attached'] = [ + 'library' => ['my_module/my_module.styles'], + 'drupalSettings' => [ + 'my_module' => ['endpoint' => '/api/items'], + ], +]; +``` + +## #lazy_builder — Deferred Rendering + +```php +$build['expensive'] = [ + '#lazy_builder' => [ + '\Drupal\my_module\Service\MyLazyBuilder::renderContent', + [$param1, $param2], + ], + '#create_placeholder' => TRUE, +]; +``` + +## Common Render Element Types + +`container`, `html_tag`, `item_list`, `link`, `table`, `status_messages`, `more_link`, `operations` + +## Related Files + +- [11-caching-performance.md](11-caching-performance.md) — Cache metadata details +- [20-javascript.md](20-javascript.md) — Attaching JS libraries +- [08-forms.md](08-forms.md) — Forms use render arrays diff --git a/experimental/.kb/18-migration.md b/experimental/.kb/18-migration.md new file mode 100644 index 0000000..66c7f42 --- /dev/null +++ b/experimental/.kb/18-migration.md @@ -0,0 +1,109 @@ +--- +title: Migration API +description: > + Drupal's Migration API: source, process, and destination plugins with YAML definitions and custom process plugin example. + + +tags: [migration, migrate, migrate-api, process-plugin, source, destination] +--- + +# Migration API + +## Migration YAML Definition + +```yaml +# migrations/my_migration.yml +id: my_migration +label: "My Custom Migration" +source: + plugin: csv + path: /path/to/data.csv + header_row_count: 1 + keys: + - id + column_names: + - id: [id, "Unique ID"] + - title: [title, "Title"] + +process: + title: title + body/value: body + body/format: + plugin: default_value + default_value: basic_html + type: + plugin: default_value + default_value: article + uid: + plugin: default_value + default_value: 1 + status: + plugin: default_value + default_value: 1 + +destination: + plugin: entity:node + default_bundle: article +``` + +## Custom Process Plugin + +```php +namespace Drupal\my_module\Plugin\migrate\process; + +use Drupal\migrate\ProcessPluginBase; +use Drupal\migrate\MigrateExecutableInterface; +use Drupal\migrate\Row; + +/** + * Custom process plugin. + * + * @MigrateProcessPlugin( + * id = "my_custom_process" + * ) + */ +class MyCustomProcess extends ProcessPluginBase { + + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property): mixed { + return strtoupper(trim($value)); + } +} +``` + +## Common Source Plugins + +- `csv` — CSV file (requires `migrate_source_csv`) +- `d7_node`, `d7_user` — Drupal 7 migrations +- `sql` — Direct database queries +- `url` — JSON/XML from URLs +- `embedded_data` — Inline data for testing + +## Common Process Plugins + +- `get` — Pass through value +- `default_value` — Set default +- `callback` — PHP function callback +- `concat` — Concatenate values +- `entity_lookup` — Look up entity by property +- `skip_on_empty` — Skip row if empty + +## Common Destination Plugins + +- `entity:node` — Create nodes +- `entity:user` — Create users +- `entity:taxonomy_term` — Create terms +- `config` — Write to config + +## Drush Migration Commands + +```bash +drush migrate:import my_migration # Run migration +drush migrate:rollback my_migration # Rollback +drush migrate:status # List migrations +drush migrate:messages my_migration # View messages/errors +``` + +## Related Files + +- [06-plugins.md](06-plugins.md) — Plugin system (migrate uses plugins) +- [21-workflow.md](21-workflow.md) — Drush commands diff --git a/experimental/.kb/19-composer.md b/experimental/.kb/19-composer.md new file mode 100644 index 0000000..b9e4ee1 --- /dev/null +++ b/experimental/.kb/19-composer.md @@ -0,0 +1,75 @@ +--- +title: Composer Management +description: > + Managing Drupal dependencies with Composer: adding modules, applying patches, updating core, and composer.json best practices. + + +tags: [composer, dependencies, patches, composer-json] +--- + +# Composer Management + +## Common Commands + +```bash +# Add a module +composer require drupal/admin_toolbar + +# Add a dev dependency +composer require --dev drupal/devel + +# Update a single module +composer update drupal/admin_toolbar --with-dependencies + +# Update Drupal core +composer update drupal/core --with-all-dependencies + +# Run post-update Drush commands +drush updatedb +drush config:import +drush cr +``` + +## Applying Patches + +Add the `composer-patches` plugin, then add patches to `composer.json`: + +```json +{ + "extra": { + "patches": { + "drupal/some_module": { + "Fix description": "https://www.drupal.org/files/issues/2024-01-01/issue-12345-1.patch" + } + } + } +} +``` + +## composer.json Best Practices + +- Use `drupal/core-recommended` for production +- Use `drupal/core-dev` for development (PHPUnit, PHPCS, etc.) +- Pin major versions: `"drupal/core-recommended": "^11"` +- Commit `composer.lock` to version control +- Use the `drupal.org` composer endpoint: + + ```bash + composer config repositories.drupal composer https://packages.drupal.org/8 + ``` + +## Troubleshooting + +```bash +# Composer memory issues +php -d memory_limit=-1 /usr/local/bin/composer install + +# Resolve merge conflicts in composer.lock +git checkout --theirs composer.lock +composer install +``` + +## Related Files + +- [01-project-overview.md](01-project-overview.md) — Tech stack +- [22-troubleshooting.md](22-troubleshooting.md) — Common issues diff --git a/experimental/.kb/20-javascript.md b/experimental/.kb/20-javascript.md new file mode 100644 index 0000000..77d779f --- /dev/null +++ b/experimental/.kb/20-javascript.md @@ -0,0 +1,92 @@ +--- +title: JavaScript & Frontend +description: > + Drupal's JavaScript system: Drupal behaviors, library definitions, drupalSettings, and attaching assets to render arrays and Twig templates. + + +tags: [javascript, js, drupal-behaviors, libraries, drupal-settings, frontend] +--- + +# JavaScript & Frontend + +## Drupal Behaviors (NOT jQuery document.ready) + +```javascript +// js/my-module.js +(function (Drupal, drupalSettings) { + "use strict"; + + Drupal.behaviors.myModuleBehavior = { + attach: function (context, settings) { + // Runs on every page load AND AJAX response. + const elements = context.querySelectorAll(".my-element"); + elements.forEach(function (element) { + element.addEventListener("click", handleClick); + }); + }, + detach: function (context, settings, trigger) { + // Clean up when content is removed (AJAX, etc.). + const elements = context.querySelectorAll(".my-element"); + elements.forEach(function (element) { + element.removeEventListener("click", handleClick); + }); + }, + }; + + function handleClick(event) { + // Handle click. + } +})(Drupal, drupalSettings); +``` + +**Key differences from jQuery document.ready**: + +- `attach()` fires on initial page load AND every AJAX response +- `context` scopes to the added/changed DOM fragment +- `detach()` handles cleanup for removed content +- Use vanilla JS — jQuery is deprecated in Drupal + +## Library Definition (my_module.libraries.yml) + +```yaml +my_module.styles: + version: VERSION + css: + component: + css/my-module.css: {} + js: + js/my-module.js: {} + dependencies: + - core/drupal + - core/drupalSettings +``` + +CSS weight categories (lightest to heaviest): `base`, `layout`, `component`, `state`, `theme`. + +## Attaching Libraries + +```php +// In render array (PHP) +$build['#attached']['library'][] = 'my_module/my_module.styles'; +``` + +```twig +{# In Twig template #} +{{ attach_library('my_module/my_module.styles') }} +``` + +## Passing Data to JS (drupalSettings) + +```php +$build['#attached']['drupalSettings']['my_module'] = [ + 'endpoint' => '/api/items', + 'apiKey' => $config->get('api_key'), +]; +``` + +Access in JS: `drupalSettings.my_module.endpoint` + +## Related Files + +- [17-render-api.md](17-render-api.md) — #attached property +- [08-forms.md](08-forms.md) — AJAX forms diff --git a/experimental/.kb/21-workflow.md b/experimental/.kb/21-workflow.md new file mode 100644 index 0000000..d6161fb --- /dev/null +++ b/experimental/.kb/21-workflow.md @@ -0,0 +1,95 @@ +--- +title: Development Workflow & Debugging +description: > + Essential Drush commands for development, debugging tables for cache, config, modules, entities, and performance profiling. Version control workflow conventions. + + +tags: [drush, debugging, workflow, commands, profiling, version-control] +--- + +# Development Workflow & Debugging + +## Essential Commands + +```bash +drush cr # Clear all caches +drush config:export # Export configuration +drush config:import # Import configuration +drush config:diff # Compare config with directory +drush sql:dump # Export database +drush sql:cli # Access database CLI +drush updatedb # Run database updates +``` + +## Core Debugging + +| Command | Purpose | +| --------------------------- | ------------------------------------------------------------ | +| `drush status` | Drupal root, DB connection, Drush version | +| `drush watchdog:show` | Recent log entries. Filters: `--severity=Error` `--type=php` | +| `drush watchdog:delete all` | Clear watchdog log | +| `drush sql:query "..."` | Direct SQL for large logs | + +## Cache Debugging + +| Command | Purpose | +| ---------------------------------- | ---------------------------- | +| `drush cache:rebuild` / `drush cr` | Rebuild all caches | +| `drush cache:get :` | Retrieve specific cache item | +| `drush cache:clear ` | Clear one cache bin | + +## Config Debugging + +| Command | Purpose | +| --------------------------------------- | ---------------------- | +| `drush config:get ` | Show config value | +| `drush config:set ` | Temp change | +| `drush config:export` / `drush cex` | Export config | +| `drush config:import` / `drush cim` | Import config | +| `drush config:delete ` | Remove orphaned config | + +## Module/Theme Debugging + +| Command | Purpose | +| ---------------------------------------------- | --------------------------------------- | +| `drush pm:list --type=module --status=enabled` | List enabled modules | +| `drush pm:enable ` | Enable module | +| `drush pm:uninstall ` | Fully uninstall (removes config + data) | +| `drush theme:debug` | Theme suggestions | + +## Entity & DB Debugging + +| Command | Purpose | +| ----------------------- | --------------------------------- | +| `drush sql:connect` | Show DB connection command | +| `drush entity:info` | Entity type definitions | +| `drush php` | Interactive PHP shell with Drupal | +| `drush php:eval "code"` | Execute PHP in Drupal context | + +## Performance Profiling + +```bash +drush cr # Rebuild caches +drush sql:query "EXPLAIN ANALYZE SELECT ..." # Query analysis +drush config:get system.performance # Check perf settings +# Enable Webprofiler module for detailed profiling +``` + +## Twig Debugging + +```bash +drush twig:debug # Enable/disable Twig debug mode +``` + +## Version Control + +- **Commit format**: `[#123456] Brief descriptive title` +- **Branch from**: `develop` for features +- **Atomic commits**: One logical change per commit +- **Before push**: lint + test + `drush cr` + `drush updatedb` + +## Related Files + +- [15-configuration.md](15-configuration.md) — Config workflow +- [11-caching-performance.md](11-caching-performance.md) — Caching strategies +- [22-troubleshooting.md](22-troubleshooting.md) — Common issues diff --git a/experimental/.kb/22-troubleshooting.md b/experimental/.kb/22-troubleshooting.md new file mode 100644 index 0000000..a63701d --- /dev/null +++ b/experimental/.kb/22-troubleshooting.md @@ -0,0 +1,101 @@ +--- +title: Troubleshooting Common Issues +description: > + Solutions to common Drupal development problems: installation failures, performance issues, module/theme problems, and testing configuration. + + +tags: [troubleshooting, errors, debugging, fixes, common-issues] +--- + +# Troubleshooting Common Issues + +## Installation Problems + +```bash +# Composer memory issues +php -d memory_limit=-1 /usr/local/bin/composer install + +# Permission issues +chmod 755 sites/default/files +chmod 644 sites/default/settings.php +chown -R www-data:www-data sites/default/files + +# Database connection failed +drush sql:connect # Test connection + +# PHP extensions missing +php -m # Check installed extensions +``` + +## Performance Issues + +```bash +# Identify slow queries +drush sql:query "SELECT * FROM watchdog WHERE type = 'php' ORDER BY wid DESC LIMIT 10" + +# Check cache settings +drush config:get system.performance +``` + +## Module/Theme Development Issues + +```bash +# Most issues are solved by clearing caches +drush cr + +# Service not found after adding a service +drush cr # Rebuild service container +drush config:get core.extension # Check module is enabled + +# Twig template not loading +drush cr # Clear theme registry +drush twig:debug # Enable debug mode to see suggestions + +# Cron issues +drush cron +drush watchdog:show --type=cron +``` + +## Testing Issues + +```bash +# PHPUnit not configured +cp web/core/phpunit.xml.dist phpunit.xml +# Edit phpunit.xml for SIMPLETEST_DB and SIMPLETEST_BASE_URL + +# Database for testing +# SIMPLETEST_DB=mysql://root:password@localhost/drupal_test +# SIMPLETEST_BASE_URL=http://127.0.0.1:8888 + +# Browser tests failing +# Install ChromeDriver: composer require --dev drupal/drupal-driver +# Or install Selenium +``` + +## White Screen of Death (WSOD) + +```bash +# Check PHP error logs +tail -f /var/log/apache2/error.log # Apache +tail -f /var/log/nginx/error.log # Nginx + +# Enable error reporting in settings.php +# $config['system.logging']['error_level'] = 'verbose'; + +# Check watchdog +drush watchdog:show --severity=Error +``` + +## "The website encountered an unexpected error" + +```bash +drush cr # Clear caches first +drush watchdog:show --severity=Error # Read error details +drush updatedb # Run pending updates +``` + +## Related Files + +- [21-workflow.md](21-workflow.md) — Debugging commands +- [11-caching-performance.md](11-caching-performance.md) — Performance tuning +- [19-composer.md](19-composer.md) — Composer issues diff --git a/experimental/AGENTS.md b/experimental/AGENTS.md new file mode 100644 index 0000000..9e234ea --- /dev/null +++ b/experimental/AGENTS.md @@ -0,0 +1,64 @@ +# AGENTS.md: AI Agent Guide for Drupal Development + +**AI Agent Instructions**: Follow these guidelines for consistent, high-quality contributions. This file contains essential rules only — detailed patterns and code examples live in the **knowledge base** at `.kb/`. Read the relevant `.kb/` files before writing any Drupal code. + +## Project Overview + +- **Core**: Drupal 10.x / 11.x (verify: `composer show drupal/core`) +- **PHP**: 8.3+ | **Drush**: 13+ | **Composer**: 2.0+ +- **Location**: Always run commands from project root + +## Code Standards (always enforced) + +- **PHP**: 2-space indent, ≤80 char lines, CamelCase classes, snake_case vars, full PHPDoc +- **YAML**: 2-space indent, lowercase keys +- **Twig**: `{{ }}` output, `{% %}` logic, always `|e` +- **Lint**: `vendor/bin/phpcs --standard=Drupal .` — reject code that fails +- See → [`.kb/02-code-standards.md`](.kb/02-code-standards.md) + +## Critical Rules (read before coding) + +- **Use dependency injection**, never `\Drupal::` static calls in services/controllers/plugins +- **Always set `accessCheck(TRUE)`** on entity queries (required Drupal 10.2+) +- **Always add `#cache` metadata** to render arrays that depend on data +- **Never use `|raw` in Twig** or `#markup` with user input +- See → [`.kb/12-anti-patterns.md`](.kb/12-anti-patterns.md) (14 rules) + +## Knowledge Base — Read What You Need + +The `.kb/` folder contains focused guides with code examples. Read only the files relevant to your current task. + +| File | When to read | +| ---------------------------------------------------------------- | ----------------------------------------------- | +| [`.kb/00-index.md`](.kb/00-index.md) | **Full index** with all topics and descriptions | +| [`.kb/01-project-overview.md`](.kb/01-project-overview.md) | Tech stack, requirements, conventions | +| [`.kb/02-code-standards.md`](.kb/02-code-standards.md) | Coding standards and linting | +| [`.kb/03-module-scaffolding.md`](.kb/03-module-scaffolding.md) | Creating a new module | +| [`.kb/04-services-di.md`](.kb/04-services-di.md) | Services, dependency injection | +| [`.kb/05-entity-api.md`](.kb/05-entity-api.md) | Entity loading, queries, creation | +| [`.kb/06-plugins.md`](.kb/06-plugins.md) | Plugin system (blocks, fields, etc.) | +| [`.kb/07-hooks.md`](.kb/07-hooks.md) | Hook implementations | +| [`.kb/08-forms.md`](.kb/08-forms.md) | Forms API (simple, config, AJAX) | +| [`.kb/09-routes-controllers.md`](.kb/09-routes-controllers.md) | Routes, controllers, access | +| [`.kb/10-security.md`](.kb/10-security.md) | Security best practices | +| [`.kb/11-caching-performance.md`](.kb/11-caching-performance.md) | Caching, performance | +| [`.kb/12-anti-patterns.md`](.kb/12-anti-patterns.md) | What NOT to do (14 rules) | +| [`.kb/13-testing.md`](.kb/13-testing.md) | Unit, Kernel, Functional tests | +| [`.kb/14-events.md`](.kb/14-events.md) | EventSubscribers | +| [`.kb/15-configuration.md`](.kb/15-configuration.md) | Config management | +| [`.kb/16-batch-queue.md`](.kb/16-batch-queue.md) | Batch API, Queue API | +| [`.kb/17-render-api.md`](.kb/17-render-api.md) | Render arrays, #attached, lazy builders | +| [`.kb/18-migration.md`](.kb/18-migration.md) | Migration API | +| [`.kb/19-composer.md`](.kb/19-composer.md) | Composer management | +| [`.kb/20-javascript.md`](.kb/20-javascript.md) | Drupal behaviors, libraries | +| [`.kb/21-workflow.md`](.kb/21-workflow.md) | Dev commands, debugging, Drush | +| [`.kb/22-troubleshooting.md`](.kb/22-troubleshooting.md) | Common issues and fixes | + +## Before Submitting Code + +```bash +vendor/bin/phpcs --standard=Drupal . # Code style +vendor/bin/phpunit # Run tests +drush cr # Clear caches +drush updatedb # Run updates +``` diff --git a/experimental/README.md b/experimental/README.md new file mode 100644 index 0000000..dbefb72 --- /dev/null +++ b/experimental/README.md @@ -0,0 +1,126 @@ +# Experimental: Knowledge Base Architecture + +> **⚠️ Experimental** — This is a new approach. The slim AGENTS.md + `.kb/` folder pattern is being tested alongside the traditional monolithic AGENTS.md files. + +## The Problem + +Traditional AGENTS.md files for Drupal are massive (~1,500 lines, ~8K tokens). Every time an AI agent processes a request — even a simple one like fixing a typo — it loads the entire file. This wastes tokens and slows down responses. + +## The Solution + +Split the monolithic AGENTS.md into a **slim entry point** (60 lines) + a **knowledge base** of focused files that the agent reads on demand. + +``` +your-drupal-project/ +├── AGENTS.md ← 60 lines (~400 tokens) — always loaded +└── .kb/ ← loaded on demand per task + ├── 00-index.md Full topic map + ├── 01-project-overview.md + ├── 02-code-standards.md + ├── 03-module-scaffolding.md + ├── 04-services-di.md + ├── 05-entity-api.md + ├── 06-plugins.md + ├── 07-hooks.md + ├── 08-forms.md + ├── 09-routes-controllers.md + ├── 10-security.md + ├── 11-caching-performance.md + ├── 12-anti-patterns.md + ├── 13-testing.md + ├── 14-events.md + ├── 15-configuration.md + ├── 16-batch-queue.md + ├── 17-render-api.md + ├── 18-migration.md + ├── 19-composer.md + ├── 20-javascript.md + ├── 21-workflow.md + └── 22-troubleshooting.md +``` + +### How It Works + +1. **AGENTS.md** is always loaded — contains project overview, critical rules, and a table telling the agent which `.kb/` file to read for each task +2. **Agent reads on demand** — When the user asks "build a form", the agent reads `.kb/08-forms.md`. When they ask "create a block plugin", it reads `.kb/06-plugins.md` +3. **Cross-references** — Each `.kb/` file links to related files so the agent can chain-read when needed +4. **Front matter** — Every file has YAML front matter with `title`, `description`, and `tags` for agent discovery + +### Token Savings + +| Scenario | Monolithic | Knowledge Base | +| -------------------------------------------- | ------------- | ------------------------ | +| Simple task (fix typo) | ~8,000 tokens | ~400 tokens | +| Build a form | ~8,000 tokens | ~400 + ~1,200 (forms.md) | +| Complex task (services + entities + caching) | ~8,000 tokens | ~400 + ~3,500 (3 files) | + +## Install + +### Quick Install (recommended) + +Run this from your Drupal project's root directory: + +```bash +# Default: Vanilla variant + knowledge base +curl -fsSL https://raw.githubusercontent.com/amazeeio/drupal-agents-md/main/experimental/install.sh | bash + +# DDEV variant +curl -fsSL https://raw.githubusercontent.com/amazeeio/drupal-agents-md/main/experimental/install.sh | bash -s -- --variant=ddev + +# Lagoon variant +curl -fsSL https://raw.githubusercontent.com/amazeeio/drupal-agents-md/main/experimental/install.sh | bash -s -- --variant=lagoon +``` + +Or with wget: + +```bash +wget -qO- https://raw.githubusercontent.com/amazeeio/drupal-agents-md/main/experimental/install.sh | bash +``` + +### Install Options + +| Option | Description | +| ------------------- | ------------------------------------------- | +| `--variant=vanilla` | Vanilla variant (default) | +| `--variant=ddev` | DDEV variant | +| `--variant=lagoon` | Lagoon variant | +| `--branch=` | Use a specific git branch (default: `main`) | +| `--kb-only` | Install only `.kb/` folder (no AGENTS.md) | +| `--no-kb` | Install only AGENTS.md (no `.kb/` folder) | +| `--force` | Overwrite without prompting | +| `--help` | Show help message | + +### Manual Install + +```bash +# Clone the repo +git clone https://github.com/amazeeio/drupal-agents-md.git /tmp/drupal-agents-md + +# Copy files to your project +cp /tmp/drupal-agents-md/experimental/AGENTS.md /path/to/your/drupal-project/ +cp -r /tmp/drupal-agents-md/experimental/.kb /path/to/your/drupal-project/ + +# Clean up +rm -rf /tmp/drupal-agents-md +``` + +## After Install + +Open your AI coding tool (Cursor, Claude Code, GitHub Copilot, etc.) in the project directory. It will automatically read `AGENTS.md` and follow its guidance to load `.kb/` files as needed. + +## File Reference + +Each `.kb/` file is self-contained with: + +- **Front matter** — `title`, `description`, `tags` for discovery +- **Code examples** — Copy-pasteable PHP, YAML, Twig, JavaScript snippets +- **Cross-references** — Links to related `.kb/` files +- **Related Files section** — At the bottom of every file + +## Feedback + +This is experimental. If you try it, open an issue at [amazeeio/drupal-agents-md](https://github.com/amazeeio/drupal-agents-md/issues) with: + +- Which AI tool you used (Cursor, Claude Code, Copilot, etc.) +- Whether the agent correctly loaded `.kb/` files on demand +- Any tasks where the agent needed info that wasn't in the `.kb/` files diff --git a/experimental/install.sh b/experimental/install.sh new file mode 100755 index 0000000..4d72198 --- /dev/null +++ b/experimental/install.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env bash +# +# install.sh — Download and install Drupal AGENTS.md + knowledge base +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/amazeeio/drupal-agents-md/main/experimental/install.sh | bash +# +# Or with wget: +# wget -qO- https://raw.githubusercontent.com/amazeeio/drupal-agents-md/main/experimental/install.sh | bash +# +# Options: +# --variant=vanilla Install Vanilla variant (default) +# --variant=ddev Install DDEV variant +# --variant=lagoon Install Lagoon variant +# --branch=name Use a specific git branch (default: main) +# --kb-only Install only the .kb/ folder (no AGENTS.md) +# --no-kb Install only the AGENTS.md file (no .kb/ folder) +# --force Overwrite existing files without prompting +# --help Show this help message +# +set -euo pipefail + +REPO_URL="https://github.com/amazeeio/drupal-agents-md.git" +REPO_BRANCH="main" +REPO_DIR="" +VARIANT="vanilla" +INCLUDE_KB=true +INCLUDE_AGENTS=true +FORCE=false +TARGET_DIR="$(pwd)" + +# ─── Colors ─────────────────────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +info() { echo -e "${BLUE}[INFO]${NC} $*"; } +ok() { echo -e "${GREEN}[OK]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# ─── Parse arguments ────────────────────────────────────────────────────────── +for arg in "$@"; do + case "$arg" in + --variant=*) + VARIANT="${arg#--variant=}" + ;; + --kb-only) + INCLUDE_AGENTS=false + INCLUDE_KB=true + ;; + --no-kb) + INCLUDE_AGENTS=true + INCLUDE_KB=false + ;; + --branch=*) + REPO_BRANCH="${arg#--branch=}" + ;; + --force) + FORCE=true + ;; + --help|-h) + head -20 "$0" | grep '^#' | sed 's/^# \?//' + exit 0 + ;; + *) + error "Unknown option: $arg" + error "Run with --help for usage." + exit 1 + ;; + esac +done + +# ─── Validate variant ───────────────────────────────────────────────────────── +VARIANT=$(echo "$VARIANT" | tr '[:upper:]' '[:lower:]') +case "$VARIANT" in + vanilla|ddev|lagoon) ;; + *) + error "Unknown variant '$VARIANT'. Choose: vanilla, ddev, lagoon" + exit 1 + ;; +esac + +# ─── Preflight checks ──────────────────────────────────────────────────────── +if ! command -v git &>/dev/null; then + error "git is required but not found. Install git first." + exit 1 +fi + +if [ "$INCLUDE_AGENTS" = false ] && [ "$INCLUDE_KB" = false ]; then + error "Cannot use both --kb-only and --no-kb at the same time." + exit 1 +fi + +# ─── Header ─────────────────────────────────────────────────────────────────── +echo "" +echo " ╔═══════════════════════════════════════════════════════════╗" +echo " ║ Drupal AGENTS.md — Knowledge Base Installer ║" +echo " ╚═══════════════════════════════════════════════════════════╝" +echo "" +info "Variant: ${VARIANT}" +info "Branch: ${REPO_BRANCH}" +info "Target: ${TARGET_DIR}" +info "AGENTS.md: $([ "$INCLUDE_AGENTS" = true ] && echo "yes" || echo "no")" +info ".kb/ folder: $([ "$INCLUDE_KB" = true ] && echo "yes" || echo "no")" +echo "" + +# ─── Check for existing files ───────────────────────────────────────────────── +if [ "$FORCE" = false ]; then + conflicts=() + if [ "$INCLUDE_AGENTS" = true ] && [ -f "${TARGET_DIR}/AGENTS.md" ]; then + conflicts+=("AGENTS.md") + fi + if [ "$INCLUDE_KB" = true ] && [ -d "${TARGET_DIR}/.kb" ]; then + conflicts+=(".kb/") + fi + + if [ ${#conflicts[@]} -gt 0 ]; then + warn "Found existing files: ${conflicts[*]}" + echo -n " Overwrite? [y/N] " + read -r answer + if [ "$answer" != "y" ] && [ "$answer" != "Y" ]; then + info "Aborted." + exit 0 + fi + fi +fi + +# ─── Clone repo to temp directory ───────────────────────────────────────────── +info "Cloning repository..." +REPO_DIR=$(mktemp -d) +trap 'rm -rf "$REPO_DIR"' EXIT + +if ! git clone --depth 1 --branch "$REPO_BRANCH" --quiet "$REPO_URL" "$REPO_DIR" 2>/dev/null; then + error "Failed to clone repository (branch: ${REPO_BRANCH}). Check your internet connection." + exit 1 +fi +ok "Repository cloned" + +# ─── Validate source files exist ────────────────────────────────────────────── +SOURCE_DIR="${REPO_DIR}/experimental" + +if [ "$INCLUDE_AGENTS" = true ]; then + if [ ! -f "${SOURCE_DIR}/AGENTS.md" ]; then + error "AGENTS.md not found in repository at ${SOURCE_DIR}/AGENTS.md" + exit 1 + fi +fi + +if [ "$INCLUDE_KB" = true ]; then + if [ ! -d "${SOURCE_DIR}/.kb" ]; then + error ".kb/ folder not found in repository at ${SOURCE_DIR}/.kb" + exit 1 + fi +fi + +# ─── Apply variant-specific path in AGENTS.md ───────────────────────────────── +# The experimental AGENTS.md uses .kb/ relative paths. +# For DDEV/Lagoon variants, we also copy the variant-specific content. +VARIANT_AGENTS="${REPO_DIR}/${VARIANT^^}/AGENTS.md" +# Normalize: DDEV stays uppercase, Vanilla/Lagoon need title case +case "$VARIANT" in + ddev) VARIANT_AGENTS="${REPO_DIR}/DDEV/AGENTS.md" ;; + vanilla) VARIANT_AGENTS="${REPO_DIR}/Vanilla/AGENTS.md" ;; + lagoon) VARIANT_AGENTS="${REPO_DIR}/Lagoon/AGENTS.md" ;; +esac + +# ─── Install files ──────────────────────────────────────────────────────────── +if [ "$INCLUDE_AGENTS" = true ]; then + # Use the experimental slim AGENTS.md (with .kb/ references) + cp "${SOURCE_DIR}/AGENTS.md" "${TARGET_DIR}/AGENTS.md" + + # If using a non-vanilla variant, append a note about the variant + if [ "$VARIANT" != "vanilla" ]; then + # Prepend variant notice to the AGENTS.md + VARIANT_NOTE="\n\n" + # Use a temp file for portability + echo -e "${VARIANT_NOTE}" | cat - "${TARGET_DIR}/AGENTS.md" > "${TARGET_DIR}/AGENTS.md.tmp" && mv "${TARGET_DIR}/AGENTS.md.tmp" "${TARGET_DIR}/AGENTS.md" + fi + + ok "Installed AGENTS.md" +fi + +if [ "$INCLUDE_KB" = true ]; then + # Remove existing .kb/ if present + if [ -d "${TARGET_DIR}/.kb" ]; then + rm -rf "${TARGET_DIR}/.kb" + fi + + # Copy .kb/ folder + cp -r "${SOURCE_DIR}/.kb" "${TARGET_DIR}/.kb" + + # Count installed files + KB_FILES=$(find "${TARGET_DIR}/.kb" -name "*.md" | wc -l | tr -d ' ') + ok "Installed .kb/ folder (${KB_FILES} files)" +fi + +# ─── Done ───────────────────────────────────────────────────────────────────── +echo "" +echo " ┌─────────────────────────────────────────────────────────┐" +echo " │ ✅ Installation complete! │" +echo " └─────────────────────────────────────────────────────────┘" +echo "" +info "Files installed in: ${TARGET_DIR}" +if [ "$INCLUDE_AGENTS" = true ]; then + info " - AGENTS.md (AI agents will read this automatically)" +fi +if [ "$INCLUDE_KB" = true ]; then + info " - .kb/ (knowledge base — loaded on demand by the agent)" +fi +echo "" +info "Your AI coding agent (Cursor, Claude Code, etc.) will automatically" +info "read AGENTS.md and follow its guidance to load .kb/ files as needed." +echo "" diff --git a/package.json b/package.json new file mode 100644 index 0000000..6210190 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "drupal-agents-md", + "private": true, + "description": "AI Agent Development Guides for Drupal", + "scripts": { + "format": "prettier --write '**/*.md'", + "lint": "markdownlint '**/*.md'", + "lint:fix": "prettier --write '**/*.md' && markdownlint --fix '**/*.md'", + "check": "markdownlint '**/*.md' && prettier --check '**/*.md'" + }, + "devDependencies": { + "markdownlint-cli": "^0.44.0", + "prettier": "^3.5.0" + } +}