From ad496ee23b529aecd110886a6190d83f32901704 Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Thu, 23 Apr 2026 09:08:56 +0300 Subject: [PATCH 1/6] feat(ci): add GitHub Actions workflow for markdown linting and validation --- .github/workflows/validate.yml | 119 +++ .gitignore | 7 + CHANGELOG.md | 60 ++ CONTRIBUTING.md | 133 +++ DDEV/AGENTS.md | 1393 ++++++++++++++++++++++++++----- Lagoon/AGENTS.md | 1369 ++++++++++++++++++++++++++++++ README.md | 94 ++- Vanilla/AGENTS.md | 1425 ++++++++++++++++++++++++++------ 8 files changed, 4135 insertions(+), 465 deletions(-) create mode 100644 .github/workflows/validate.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 Lagoon/AGENTS.md diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..7c1cd22 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,119 @@ +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 markdownlint-cli + run: npm install -g markdownlint-cli + + - name: Lint Markdown files + run: | + markdownlint \ + --disable MD013 MD033 MD041 MD034 \ + -- \ + *.md \ + DDEV/AGENTS.md \ + Vanilla/AGENTS.md \ + Lagoon/AGENTS.md + + - name: Check markdown code fences + run: | + errors=0 + for file in DDEV/AGENTS.md Vanilla/AGENTS.md Lagoon/AGENTS.md; do + # Count opening and closing fences + 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)) + else + echo "OK: $file has balanced code fences ($opens markers)" + fi + done + exit $errors + + - name: Validate YAML code blocks + run: | + errors=0 + for file in DDEV/AGENTS.md Vanilla/AGENTS.md Lagoon/AGENTS.md; do + echo "Checking YAML blocks in $file..." + # Extract YAML code blocks and validate them + 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 YAML block {i+1}: {e}') + sys.exit(1) + print(f' OK: {len(blocks)} YAML blocks validated') + " || errors=$((errors + 1)) + done + exit $errors + + - name: Check internal links + run: | + errors=0 + for file in README.md CONTRIBUTING.md CHANGELOG.md; do + if [ ! -f "$file" ]; then continue; fi + echo "Checking links in $file..." + # Check that local file references exist + while IFS= read -r link; do + target=$(echo "$link" | sed 's/\[.*\](\(.*\))/\1/' | cut -d'#' -f1) + if [ -n "$target" ] && [ ! -f "$target" ]; then + echo " ERROR: Broken link '$target' in $file" + errors=$((errors + 1)) + fi + done < <(grep -oE '\]\([^)]+\)' "$file" | grep -v 'http' || true) + done + 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 + + - name: Check required sections + run: | + required_sections=( + "## Table of Contents" + "## Code Style and Standards" + "## Anti-Patterns" + "## Module Scaffolding Template" + "## Testing" + ) + errors=0 + for file in DDEV/AGENTS.md Vanilla/AGENTS.md Lagoon/AGENTS.md; do + echo "Checking sections in $file..." + for section in "${required_sections[@]}"; do + if ! grep -q "$section" "$file"; then + echo " ERROR: Missing section '$section'" + errors=$((errors + 1)) + fi + done + echo " Section check complete" + done + exit $errors diff --git a/.gitignore b/.gitignore index adc82e1..8504880 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ # Drupal AGENTS.md Project .gitignore .claude +.cursor +.DS_Store +*.swp +*.swo +*~ +.vscode/ +.idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..52b96a5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,60 @@ +# 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..73bdd85 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,133 @@ +# 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..2b25dac 100644 --- a/DDEV/AGENTS.md +++ b/DDEV/AGENTS.md @@ -2,12 +2,28 @@ **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 @@ -29,7 +45,7 @@ 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 @@ -83,7 +99,7 @@ Create `.ddev/config.yaml` for project-specific settings: # .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,6 +113,105 @@ 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. @@ -122,54 +237,429 @@ 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 @@ -179,6 +669,9 @@ Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and - **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` @@ -189,14 +682,232 @@ 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 @@ -224,63 +935,45 @@ 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.). | +| `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).| +| `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. | +| `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. | +| `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 @@ -296,9 +989,6 @@ 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 @@ -321,93 +1011,422 @@ ddev exec drush site:status # System status check - **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**: +```php +namespace Drupal\my_module\EventSubscriber; -# Run specific tests -ddev exec vendor/bin/phpunit --filter MyModuleUnitTest -ddev exec vendor/bin/phpunit web/modules/custom/my_module/tests/src/Unit/ +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Event\RequestEvent; -# Run with custom configuration -SIMPLETEST_DB=sqlite://localhost/tmp.sqlite ddev exec vendor/bin/phpunit +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`: +```yaml + my_module.event_subscriber: + class: Drupal\my_module\EventSubscriber\MyEventSubscriber + tags: + - { name: event_subscriber } +``` -### 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 +**Drupal-specific events**: `HookEventDispatcher` module provides events for most Drupal hooks. Core events include entity events (`EntityBase::create()`, presave, etc.) and kernel events. -# Security scanning -ddev exec vendor/bin/drupal-check # Check for deprecated code -ddev exec composer audit # Check for security advisories +### Configuration Management -# Accessibility testing -ddev exec vendor/bin/phpunit --group accessibility # Accessibility tests +**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 @@ -417,14 +1436,12 @@ ddev exec drush updatedb # Run updates # 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 @@ -463,55 +1480,17 @@ 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 @@ -524,6 +1503,8 @@ SIMPLETEST_BASE_URL=http://my-drupal-project.ddev.site - **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 diff --git a/Lagoon/AGENTS.md b/Lagoon/AGENTS.md new file mode 100644 index 0000000..7937074 --- /dev/null +++ b/Lagoon/AGENTS.md @@ -0,0 +1,1369 @@ +# 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..b9f4e30 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ This repository contains specialized AGENTS.md files designed for AI coding agen ## 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) @@ -35,43 +36,68 @@ 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** +### 🧪 **Testing & Quality** (with code examples) - 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** @@ -85,26 +111,36 @@ Each AGENTS.md file contains: - 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 @@ -112,23 +148,25 @@ 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..fffd06c 100644 --- a/Vanilla/AGENTS.md +++ b/Vanilla/AGENTS.md @@ -2,26 +2,143 @@ **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. @@ -48,54 +165,429 @@ 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 @@ -105,26 +597,52 @@ Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and - **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 @@ -139,6 +657,199 @@ 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 @@ -167,62 +878,52 @@ 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.). | +| `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).| +| `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. | +| `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. | +| `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 @@ -232,7 +933,7 @@ 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 @@ -241,124 +942,423 @@ drush site:status # System status check - **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**: +```php +namespace Drupal\my_module\EventSubscriber; -# Run specific tests -vendor/bin/phpunit --filter MyModuleUnitTest -vendor/bin/phpunit modules/custom/my_module/tests/src/Unit/ +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Event\RequestEvent; -# Run with custom configuration -SIMPLETEST_DB=sqlite://localhost/tmp.sqlite vendor/bin/phpunit +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`: +```yaml + my_module.event_subscriber: + class: Drupal\my_module\EventSubscriber\MyEventSubscriber + tags: + - { name: event_subscriber } +``` -### Code Quality Tools -```bash -# Static analysis (add to composer require) -vendor/bin/phpstan analyse # PHPStan analysis -vendor/bin/psalm # Psalm analysis +**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' +``` -# Security scanning -vendor/bin/drupal-check # Check for deprecated code -composer audit # Check for security advisories +**Config install** (`config/install/my_module.settings.yml`): +```yaml +api_key: '' +max_items: 50 +enabled_types: + - article +``` -# Accessibility testing -vendor/bin/phpunit --group accessibility # Accessibility tests +**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'); ``` -### JavaScript Testing +**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 +``` + +**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') +}} ``` -## 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 +### 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 @@ -372,13 +1372,11 @@ 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 @@ -388,6 +1386,7 @@ drush sql:query "SELECT * FROM watchdog WHERE type = 'php' ORDER BY wid DESC LIM # Check cache settings drush config:get system.performance +``` ### Module/Theme Development Issues ```bash @@ -406,55 +1405,17 @@ 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 @@ -462,6 +1423,8 @@ SIMPLETEST_BASE_URL=http://127.0.0.1:8888 - **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 From d0ae8710022fafb6f215d9f5300204c84d350ab2 Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Thu, 23 Apr 2026 09:59:43 +0300 Subject: [PATCH 2/6] feat(docs): add comprehensive AI agent guide for Drupal development --- experimental/.kb/00-index.md | 71 ++++++++++ experimental/.kb/01-project-overview.md | 47 +++++++ experimental/.kb/02-code-standards.md | 57 ++++++++ experimental/.kb/03-module-scaffolding.md | 90 +++++++++++++ experimental/.kb/04-services-di.md | 107 +++++++++++++++ experimental/.kb/05-entity-api.md | 90 +++++++++++++ experimental/.kb/06-plugins.md | 79 +++++++++++ experimental/.kb/07-hooks.md | 85 ++++++++++++ experimental/.kb/08-forms.md | 144 +++++++++++++++++++++ experimental/.kb/09-routes-controllers.md | 124 ++++++++++++++++++ experimental/.kb/10-security.md | 66 ++++++++++ experimental/.kb/11-caching-performance.md | 89 +++++++++++++ experimental/.kb/12-anti-patterns.md | 38 ++++++ experimental/.kb/13-testing.md | 131 +++++++++++++++++++ experimental/.kb/14-events.md | 69 ++++++++++ experimental/.kb/15-configuration.md | 84 ++++++++++++ experimental/.kb/16-batch-queue.md | 111 ++++++++++++++++ experimental/.kb/17-render-api.md | 88 +++++++++++++ experimental/.kb/18-migration.md | 105 +++++++++++++++ experimental/.kb/19-composer.md | 67 ++++++++++ experimental/.kb/20-javascript.md | 87 +++++++++++++ experimental/.kb/21-workflow.md | 90 +++++++++++++ experimental/.kb/22-troubleshooting.md | 93 +++++++++++++ experimental/AGENTS.md | 60 +++++++++ 24 files changed, 2072 insertions(+) create mode 100644 experimental/.kb/00-index.md create mode 100644 experimental/.kb/01-project-overview.md create mode 100644 experimental/.kb/02-code-standards.md create mode 100644 experimental/.kb/03-module-scaffolding.md create mode 100644 experimental/.kb/04-services-di.md create mode 100644 experimental/.kb/05-entity-api.md create mode 100644 experimental/.kb/06-plugins.md create mode 100644 experimental/.kb/07-hooks.md create mode 100644 experimental/.kb/08-forms.md create mode 100644 experimental/.kb/09-routes-controllers.md create mode 100644 experimental/.kb/10-security.md create mode 100644 experimental/.kb/11-caching-performance.md create mode 100644 experimental/.kb/12-anti-patterns.md create mode 100644 experimental/.kb/13-testing.md create mode 100644 experimental/.kb/14-events.md create mode 100644 experimental/.kb/15-configuration.md create mode 100644 experimental/.kb/16-batch-queue.md create mode 100644 experimental/.kb/17-render-api.md create mode 100644 experimental/.kb/18-migration.md create mode 100644 experimental/.kb/19-composer.md create mode 100644 experimental/.kb/20-javascript.md create mode 100644 experimental/.kb/21-workflow.md create mode 100644 experimental/.kb/22-troubleshooting.md create mode 100644 experimental/AGENTS.md diff --git a/experimental/.kb/00-index.md b/experimental/.kb/00-index.md new file mode 100644 index 0000000..1329bca --- /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..ea64270 --- /dev/null +++ b/experimental/.kb/01-project-overview.md @@ -0,0 +1,47 @@ +--- +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..8560b1c --- /dev/null +++ b/experimental/.kb/02-code-standards.md @@ -0,0 +1,57 @@ +--- +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..456095c --- /dev/null +++ b/experimental/.kb/03-module-scaffolding.md @@ -0,0 +1,90 @@ +--- +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..b75ce03 --- /dev/null +++ b/experimental/.kb/04-services-di.md @@ -0,0 +1,107 @@ +--- +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..da42ab2 --- /dev/null +++ b/experimental/.kb/05-entity-api.md @@ -0,0 +1,90 @@ +--- +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..4aee4b5 --- /dev/null +++ b/experimental/.kb/06-plugins.md @@ -0,0 +1,79 @@ +--- +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..3b8d088 --- /dev/null +++ b/experimental/.kb/07-hooks.md @@ -0,0 +1,85 @@ +--- +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..621b7fe --- /dev/null +++ b/experimental/.kb/08-forms.md @@ -0,0 +1,144 @@ +--- +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..54d60da --- /dev/null +++ b/experimental/.kb/09-routes-controllers.md @@ -0,0 +1,124 @@ +--- +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..d1e37f7 --- /dev/null +++ b/experimental/.kb/10-security.md @@ -0,0 +1,66 @@ +--- +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..ea6c9c0 --- /dev/null +++ b/experimental/.kb/11-caching-performance.md @@ -0,0 +1,89 @@ +--- +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..c29558c --- /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..d43a5f5 --- /dev/null +++ b/experimental/.kb/13-testing.md @@ -0,0 +1,131 @@ +--- +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..7bd3bb3 --- /dev/null +++ b/experimental/.kb/14-events.md @@ -0,0 +1,69 @@ +--- +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..5807e0e --- /dev/null +++ b/experimental/.kb/15-configuration.md @@ -0,0 +1,84 @@ +--- +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..895c702 --- /dev/null +++ b/experimental/.kb/16-batch-queue.md @@ -0,0 +1,111 @@ +--- +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..21c58d5 --- /dev/null +++ b/experimental/.kb/17-render-api.md @@ -0,0 +1,88 @@ +--- +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..c47254a --- /dev/null +++ b/experimental/.kb/18-migration.md @@ -0,0 +1,105 @@ +--- +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..9e3aa4c --- /dev/null +++ b/experimental/.kb/19-composer.md @@ -0,0 +1,67 @@ +--- +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..c75947a --- /dev/null +++ b/experimental/.kb/20-javascript.md @@ -0,0 +1,87 @@ +--- +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..f29f3aa --- /dev/null +++ b/experimental/.kb/21-workflow.md @@ -0,0 +1,90 @@ +--- +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..3c38f41 --- /dev/null +++ b/experimental/.kb/22-troubleshooting.md @@ -0,0 +1,93 @@ +--- +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..49b6ca1 --- /dev/null +++ b/experimental/AGENTS.md @@ -0,0 +1,60 @@ +# 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 +``` From b25e0b448e8cd351a93fc48326f949191015e8cb Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Thu, 23 Apr 2026 10:15:20 +0300 Subject: [PATCH 3/6] chore(CI): update GitHub Actions workflow to improve markdown linting and formatting --- .github/workflows/validate.yml | 73 ++------ .gitignore | 2 + .markdownlint.json | 15 ++ .markdownlintignore | 1 + .prettierignore | 1 + .prettierrc | 4 + CHANGELOG.md | 3 + CONTRIBUTING.md | 12 +- DDEV/AGENTS.md | 184 ++++++++++++++------- Lagoon/AGENTS.md | 132 ++++++++++----- README.md | 30 ++-- Vanilla/AGENTS.md | 183 +++++++++++++------- experimental/.kb/00-index.md | 50 +++--- experimental/.kb/01-project-overview.md | 11 +- experimental/.kb/02-code-standards.md | 12 +- experimental/.kb/03-module-scaffolding.md | 12 +- experimental/.kb/04-services-di.md | 39 +++-- experimental/.kb/05-entity-api.md | 7 +- experimental/.kb/06-plugins.md | 7 +- experimental/.kb/07-hooks.md | 13 +- experimental/.kb/08-forms.md | 8 +- experimental/.kb/09-routes-controllers.md | 30 ++-- experimental/.kb/10-security.md | 15 +- experimental/.kb/11-caching-performance.md | 29 ++-- experimental/.kb/12-anti-patterns.md | 6 +- experimental/.kb/13-testing.md | 14 +- experimental/.kb/14-events.md | 30 ++-- experimental/.kb/15-configuration.md | 29 +++- experimental/.kb/16-batch-queue.md | 9 +- experimental/.kb/17-render-api.md | 39 +++-- experimental/.kb/18-migration.md | 18 +- experimental/.kb/19-composer.md | 12 +- experimental/.kb/20-javascript.md | 21 ++- experimental/.kb/21-workflow.md | 71 ++++---- experimental/.kb/22-troubleshooting.md | 12 +- experimental/AGENTS.md | 54 +++--- package.json | 15 ++ 37 files changed, 759 insertions(+), 444 deletions(-) create mode 100644 .markdownlint.json create mode 100644 .markdownlintignore create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 package.json diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 7c1cd22..d49bc5a 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -19,40 +19,31 @@ jobs: with: node-version: '20' - - name: Install markdownlint-cli - run: npm install -g markdownlint-cli + - name: Install dependencies + run: npm ci - name: Lint Markdown files - run: | - markdownlint \ - --disable MD013 MD033 MD041 MD034 \ - -- \ - *.md \ - DDEV/AGENTS.md \ - Vanilla/AGENTS.md \ - Lagoon/AGENTS.md + run: npm run lint + + - name: Check formatting + run: npx prettier --check '**/*.md' - name: Check markdown code fences run: | errors=0 - for file in DDEV/AGENTS.md Vanilla/AGENTS.md Lagoon/AGENTS.md; do - # Count opening and closing fences + 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)) - else - echo "OK: $file has balanced code fences ($opens markers)" fi - done + done < <(find . -name "*.md" -not -path "./.git/*" -not -path "./node_modules/*" -print0) exit $errors - name: Validate YAML code blocks run: | errors=0 - for file in DDEV/AGENTS.md Vanilla/AGENTS.md Lagoon/AGENTS.md; do - echo "Checking YAML blocks in $file..." - # Extract YAML code blocks and validate them + while IFS= read -r -d '' file; do python3 -c " import re, sys, yaml content = open('$file').read() @@ -61,28 +52,12 @@ jobs: try: yaml.safe_load(block) except yaml.YAMLError as e: - print(f' ERROR in YAML block {i+1}: {e}') + print(f' ERROR in $file YAML block {i+1}: {e}') sys.exit(1) - print(f' OK: {len(blocks)} YAML blocks validated') + if blocks: + print(f' OK: $file — {len(blocks)} YAML blocks') " || errors=$((errors + 1)) - done - exit $errors - - - name: Check internal links - run: | - errors=0 - for file in README.md CONTRIBUTING.md CHANGELOG.md; do - if [ ! -f "$file" ]; then continue; fi - echo "Checking links in $file..." - # Check that local file references exist - while IFS= read -r link; do - target=$(echo "$link" | sed 's/\[.*\](\(.*\))/\1/' | cut -d'#' -f1) - if [ -n "$target" ] && [ ! -f "$target" ]; then - echo " ERROR: Broken link '$target' in $file" - errors=$((errors + 1)) - fi - done < <(grep -oE '\]\([^)]+\)' "$file" | grep -v 'http' || true) - done + done < <(find . -name "*.md" -not -path "./.git/*" -not -path "./node_modules/*" -print0) exit $errors - name: Check all variants exist @@ -95,25 +70,3 @@ jobs: lines=$(wc -l < "$variant/AGENTS.md") echo "OK: $variant/AGENTS.md ($lines lines)" done - - - name: Check required sections - run: | - required_sections=( - "## Table of Contents" - "## Code Style and Standards" - "## Anti-Patterns" - "## Module Scaffolding Template" - "## Testing" - ) - errors=0 - for file in DDEV/AGENTS.md Vanilla/AGENTS.md Lagoon/AGENTS.md; do - echo "Checking sections in $file..." - for section in "${required_sections[@]}"; do - if ! grep -q "$section" "$file"; then - echo " ERROR: Missing section '$section'" - errors=$((errors + 1)) - fi - done - echo " Section check complete" - done - exit $errors diff --git a/.gitignore b/.gitignore index 8504880..1dbae2a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ *~ .vscode/ .idea/ +node_modules/ +package-lock.json 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 index 52b96a5..b478705 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ 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 @@ -49,6 +50,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **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+** @@ -56,5 +58,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - 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 index 73bdd85..0156ab1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,7 @@ Thank you for your interest in improving the Drupal AI Agent Development Guides! ### Making Changes 1. **Fork the repository** and create a feature branch: + ```bash git checkout -b feature/my-improvement ``` @@ -27,6 +28,7 @@ Thank you for your interest in improving the Drupal AI Agent Development Guides! - 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" ``` @@ -56,6 +58,7 @@ Follow [Conventional Commits](https://www.conventionalcommits.org/): ### 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 ` @@ -66,7 +69,7 @@ When adding content that applies to all variants: ### Style - Use `**bold**` for emphasis on key terms -- Use fenced code blocks with language identifiers (```php, ```yaml, ```bash) +- 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 @@ -96,16 +99,19 @@ 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: ___ +- [ ] Other: \_\_\_ ## Type of Change + - [ ] New content/section - [ ] Correction/fix - [ ] Code example addition @@ -113,7 +119,9 @@ Brief description of what this PR changes and why. - [ ] CI/tooling ## Testing + How did you verify the changes? + - [ ] Rendered markdown preview - [ ] Checked code syntax - [ ] Verified commands work in target environment diff --git a/DDEV/AGENTS.md b/DDEV/AGENTS.md index 2b25dac..2f014cb 100644 --- a/DDEV/AGENTS.md +++ b/DDEV/AGENTS.md @@ -19,6 +19,7 @@ - [Additional Resources](#additional-resources) ## Project Overview + - **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 @@ -29,6 +30,7 @@ ## DDEV Quick Setup ### Prerequisites + ```bash # Install DDEV (macOS) brew install ddev/ddev/ddev @@ -39,6 +41,7 @@ ddev --version ``` ### Initialize DDEV Project + ```bash # Clone the repository git clone my-drupal-project @@ -71,6 +74,7 @@ ddev launch ``` ### Essential DDEV Commands + ```bash # Environment management ddev start # Start development environment @@ -93,6 +97,7 @@ ddev launch # Open site in browser ``` ### DDEV Configuration + Create `.ddev/config.yaml` for project-specific settings: ```yaml @@ -182,10 +187,11 @@ web/modules/custom/my_module/ ### Minimal Module Files **my_module.info.yml**: + ```yaml -name: 'My Module' +name: "My Module" type: module -description: 'Custom module description.' +description: "Custom module description." core_version_requirement: ^10 || ^11 package: Custom dependencies: @@ -194,6 +200,7 @@ dependencies: ``` **composer.json** (for PSR-4 autoloading in tests): + ```json { "name": "drupal/my_module", @@ -213,6 +220,7 @@ dependencies: ``` ## Code Style and Standards + Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and PHPCS for enforcement. - **PHP**: @@ -226,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/ @@ -239,17 +248,19 @@ Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and ### 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'] + 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; @@ -285,6 +296,7 @@ class MyController extends ControllerBase { ### Entity API & Queries **Loading entities**: + ```php // Single entity $node = \Drupal::entityTypeManager()->getStorage('node')->load(123); @@ -300,6 +312,7 @@ $nodes = \Drupal::entityTypeManager()->getStorage('node')->loadByProperties([ ``` **Entity queries** (always prefer over raw SQL): + ```php use Drupal\Core\Entity\Query\QueryInterface; @@ -317,6 +330,7 @@ $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($ids); ``` **Creating entities**: + ```php $node = \Drupal::entityTypeManager()->getStorage('node')->create([ 'type' => 'article', @@ -332,6 +346,7 @@ $node->save(); ``` **Field access**: Use entity field API instead of direct property access: + ```php // Correct $node->get('field_my_field')->value; @@ -344,6 +359,7 @@ $node->field_my_field->value; // Magic __get — works but less explicit ### Plugin System **Block plugin example**: + ```php namespace Drupal\my_module\Plugin\Block; @@ -445,6 +461,7 @@ function my_module_cron(): void { ### Forms API **Simple form**: + ```php namespace Drupal\my_module\Form; @@ -515,6 +532,7 @@ class CustomForm extends FormBase { ``` **Configuration form**: + ```php namespace Drupal\my_module\Form; @@ -566,26 +584,27 @@ class SettingsForm extends ConfigFormBase { ### Routes & Controllers **Route definition** (`my_module.routing.yml`): + ```yaml my_module.content: - path: '/my-module/{node}' + path: "/my-module/{node}" defaults: _controller: '\Drupal\my_module\Controller\MyController::content' - _title: 'My Module Page' + _title: "My Module Page" requirements: - _permission: 'access content' + _permission: "access content" node: \d+ my_module.settings: - path: '/admin/config/my-module/settings' + path: "/admin/config/my-module/settings" defaults: _form: '\Drupal\my_module\Form\SettingsForm' - _title: 'My Module Settings' + _title: "My Module Settings" requirements: - _permission: 'administer site configuration' + _permission: "administer site configuration" my_module.custom_access: - path: '/my-module/custom/{node}' + path: "/my-module/custom/{node}" defaults: _controller: '\Drupal\my_module\Controller\MyController::customPage' _title_callback: '\Drupal\my_module\Controller\MyController::pageTitle' @@ -594,6 +613,7 @@ my_module.custom_access: ``` **Controller**: + ```php namespace Drupal\my_module\Controller; @@ -636,6 +656,7 @@ class MyController extends ControllerBase { ``` **Custom access checker**: + ```php namespace Drupal\my_module\Access; @@ -654,16 +675,18 @@ class MyAccessChecker implements AccessInterface { ``` Register in `my_module.services.yml`: + ```yaml - my_module.access_checker: - class: Drupal\my_module\Access\MyAccessChecker - tags: - - { name: access_check } +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 @@ -674,6 +697,7 @@ Register in `my_module.services.yml`: - **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 @@ -685,6 +709,7 @@ Register in `my_module.services.yml`: - **Entity loading**: Load multiple entities at once with `loadMultiple()` instead of individual loads **Render array with caching**: + ```php $build = [ '#theme' => 'item_list', @@ -699,6 +724,7 @@ $build = [ ``` **Lazy builder for expensive operations**: + ```php $build['expensive_content'] = [ '#lazy_builder' => [ @@ -710,6 +736,7 @@ $build['expensive_content'] = [ ``` ### Caching Strategies + - **Render cache**: Cache complex markup with proper tags/contexts - **Dynamic page cache**: Automatically handles cacheability for anonymous users - **Internal page cache**: Serves full cached pages for anonymous users @@ -751,6 +778,7 @@ These are common mistakes that an AI agent must avoid: ## Testing & Quality Assurance ### PHPUnit Testing Framework + Aim for ≥ 80% code coverage. Drupal provides multiple test types: ```bash @@ -772,6 +800,7 @@ 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; @@ -800,6 +829,7 @@ class MyServiceTest extends UnitTestCase { ``` ### Kernel Test Example + ```php // tests/src/Kernel/MyModuleKernelTest.php namespace Drupal\Tests\my_module\Kernel; @@ -837,6 +867,7 @@ class MyModuleKernelTest extends KernelTestBase { ``` ### Functional Test Example + ```php // tests/src/Functional/MyModuleFunctionalTest.php namespace Drupal\Tests\my_module\Functional; @@ -877,6 +908,7 @@ class MyModuleFunctionalTest extends BrowserTestBase { ``` ### Code Quality Tools in DDEV + ```bash # Static analysis (add to composer require) ddev exec vendor/bin/phpstan analyse # PHPStan analysis @@ -891,6 +923,7 @@ ddev exec vendor/bin/phpunit --group accessibility # Accessibility tests ``` ### JavaScript Testing + ```bash # Install JavaScript dependencies ddev exec npm install @@ -901,6 +934,7 @@ ddev exec npm run test:a11y # Accessibility tests ``` ### Before Submitting Code + ```bash # Quality checklist ddev exec vendor/bin/phpcs --standard=Drupal . # Code style @@ -912,12 +946,14 @@ 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 @@ -935,47 +971,53 @@ ddev exec drush updatedb # Run database updates ### Debugging in DDEV #### Core Debugging & Information Commands -| 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 | + +| 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 | -| `ddev exec drush cache:get :` | Retrieve a specific cache item | -| `ddev exec drush cache:clear ` | Clear only one cache bin | + +| 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 | -| `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 | + +| 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 | -| `ddev exec drush theme:debug` | Lists all theme suggestions | + +| 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 | -| `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 | + +| 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: @@ -995,6 +1037,7 @@ ddev describe # Check environment status ``` ### Performance Profiling in DDEV + ```bash # Performance analysis ddev exec drush cr # Rebuild caches @@ -1006,6 +1049,7 @@ 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 @@ -1018,6 +1062,7 @@ ddev exec drush site:status # System status check Prefer EventSubscribers over hooks for many use cases. They are more testable and follow Symfony conventions. **EventSubscriber example**: + ```php namespace Drupal\my_module\EventSubscriber; @@ -1047,11 +1092,12 @@ class MyEventSubscriber implements EventSubscriberInterface { ``` Register in `my_module.services.yml`: + ```yaml - my_module.event_subscriber: - class: Drupal\my_module\EventSubscriber\MyEventSubscriber - tags: - - { name: event_subscriber } +my_module.event_subscriber: + class: Drupal\my_module\EventSubscriber\MyEventSubscriber + tags: + - { name: event_subscriber } ``` **Drupal-specific events**: `HookEventDispatcher` module provides events for most Drupal hooks. Core events include entity events (`EntityBase::create()`, presave, etc.) and kernel events. @@ -1059,34 +1105,37 @@ Register in `my_module.services.yml`: ### Configuration Management **Config schema** (`config/schema/my_module.schema.yml`): + ```yaml my_module.settings: type: config_object - label: 'My Module settings' + label: "My Module settings" mapping: api_key: type: string - label: 'API Key' + label: "API Key" max_items: type: integer - label: 'Maximum items' + label: "Maximum items" enabled_types: type: sequence - label: 'Enabled content types' + label: "Enabled content types" sequence: type: string - label: 'Content type' + label: "Content type" ``` **Config install** (`config/install/my_module.settings.yml`): + ```yaml -api_key: '' +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'); @@ -1096,6 +1145,7 @@ $value = \Drupal::config('my_module.settings')->get('api_key'); ``` **Config workflow**: + ```bash # Export all configuration ddev exec drush config:export @@ -1206,6 +1256,7 @@ class MyQueueWorker extends QueueWorkerBase implements ContainerFactoryPluginInt ``` **Adding items to the queue**: + ```php \Drupal::queue('my_module_processor')->createItem(['type' => 'cleanup', 'node_id' => 123]); ``` @@ -1215,6 +1266,7 @@ class MyQueueWorker extends QueueWorkerBase implements ContainerFactoryPluginInt - **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 @@ -1300,6 +1352,7 @@ destination: ``` **Custom process plugin**: + ```php namespace Drupal\my_module\Plugin\migrate\process; @@ -1348,6 +1401,7 @@ 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 @@ -1357,26 +1411,27 @@ ddev exec drush cr ### JavaScript & Frontend **Drupal behaviors** (not jQuery document.ready): + ```javascript // js/my-module.js (function (Drupal, drupalSettings) { - 'use strict'; + "use strict"; Drupal.behaviors.myModuleBehavior = { attach: function (context, settings) { // Run on every page load and AJAX response. - const elements = context.querySelectorAll('.my-element'); + const elements = context.querySelectorAll(".my-element"); elements.forEach(function (element) { - element.addEventListener('click', handleClick); + element.addEventListener("click", handleClick); }); }, detach: function (context, settings, trigger) { // Clean up when content is removed (AJAX, etc.). - const elements = context.querySelectorAll('.my-element'); + const elements = context.querySelectorAll(".my-element"); elements.forEach(function (element) { - element.removeEventListener('click', handleClick); + element.removeEventListener("click", handleClick); }); - } + }, }; function handleClick(event) { @@ -1386,6 +1441,7 @@ ddev exec drush cr ``` **Library definition** (`my_module.libraries.yml`): + ```yaml my_module.styles: version: VERSION @@ -1400,6 +1456,7 @@ my_module.styles: ``` **Attaching libraries**: + ```php // In render array $build['#attached']['library'][] = 'my_module/my_module.styles'; @@ -1432,6 +1489,7 @@ $node->save(); ## DDEV-Specific Troubleshooting ### Common DDEV Issues + ```bash # DDEV won't start ddev poweroff && ddev start @@ -1452,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" @@ -1464,6 +1523,7 @@ ddev exec drush pm:enable memcache redis -y ``` ### Module/Theme Development Issues in DDEV + ```bash ddev exec drush cr @@ -1479,6 +1539,7 @@ ddev exec drush watchdog:show --type=cron ``` ### Testing Issues in DDEV + ```bash # PHPUnit configuration — ensure phpunit.xml.dist exists and is configured cp web/core/phpunit.xml.dist phpunit.xml @@ -1494,11 +1555,13 @@ cp web/core/phpunit.xml.dist phpunit.xml ## 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 @@ -1507,6 +1570,7 @@ cp web/core/phpunit.xml.dist phpunit.xml - **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 index 7937074..467a559 100644 --- a/Lagoon/AGENTS.md +++ b/Lagoon/AGENTS.md @@ -19,6 +19,7 @@ - [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) @@ -30,6 +31,7 @@ ## Lagoon Quick Setup ### Prerequisites + ```bash # Install Lagoon CLI (macOS) brew tap uselagoon/lagoon-cli @@ -50,9 +52,11 @@ lagoon config add \ ``` ### Lagoon Project Files + Lagoon requires these files in the repository root: **`.lagoon.yml`** — Lagoon configuration: + ```yaml docker-compose-yaml: docker-compose.yml @@ -94,12 +98,14 @@ tasks: ``` **`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 @@ -120,6 +126,7 @@ lagoon add variable --project --environment --name NAME --value ``` ### Database & File Synchronization + ```bash # Sync database from production to local lagoon-sync sync mariadb -p -e main -t local @@ -136,19 +143,21 @@ 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) | +| 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 | +| `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'; @@ -167,6 +176,7 @@ else { ``` ### Drush Aliases + Lagoon provides Drush aliases automatically. Use them for remote operations: ```bash @@ -251,10 +261,11 @@ web/modules/custom/my_module/ ### Minimal Module Files **my_module.info.yml**: + ```yaml -name: 'My Module' +name: "My Module" type: module -description: 'Custom module description.' +description: "Custom module description." core_version_requirement: ^10 || ^11 package: Custom dependencies: @@ -263,6 +274,7 @@ dependencies: ``` **composer.json** (for PSR-4 autoloading in tests): + ```json { "name": "drupal/my_module", @@ -282,6 +294,7 @@ dependencies: ``` ## Code Style and Standards + Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and PHPCS for enforcement. - **PHP**: @@ -295,6 +308,7 @@ Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and - **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/ @@ -308,17 +322,19 @@ Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and ### 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'] + 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; @@ -354,6 +370,7 @@ class MyController extends ControllerBase { ### Entity API & Queries **Loading entities**: + ```php // Single entity $node = \Drupal::entityTypeManager()->getStorage('node')->load(123); @@ -369,6 +386,7 @@ $nodes = \Drupal::entityTypeManager()->getStorage('node')->loadByProperties([ ``` **Entity queries** (always prefer over raw SQL): + ```php use Drupal\Core\Entity\Query\QueryInterface; @@ -386,6 +404,7 @@ $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($ids); ``` **Creating entities**: + ```php $node = \Drupal::entityTypeManager()->getStorage('node')->create([ 'type' => 'article', @@ -401,6 +420,7 @@ $node->save(); ``` **Field access**: Use entity field API instead of direct property access: + ```php // Correct $node->get('field_my_field')->value; @@ -413,6 +433,7 @@ $node->field_my_field->value; // Magic __get — works but less explicit ### Plugin System **Block plugin example**: + ```php namespace Drupal\my_module\Plugin\Block; @@ -514,6 +535,7 @@ function my_module_cron(): void { ### Forms API **Simple form**: + ```php namespace Drupal\my_module\Form; @@ -584,6 +606,7 @@ class CustomForm extends FormBase { ``` **Configuration form**: + ```php namespace Drupal\my_module\Form; @@ -635,26 +658,27 @@ class SettingsForm extends ConfigFormBase { ### Routes & Controllers **Route definition** (`my_module.routing.yml`): + ```yaml my_module.content: - path: '/my-module/{node}' + path: "/my-module/{node}" defaults: _controller: '\Drupal\my_module\Controller\MyController::content' - _title: 'My Module Page' + _title: "My Module Page" requirements: - _permission: 'access content' + _permission: "access content" node: \d+ my_module.settings: - path: '/admin/config/my-module/settings' + path: "/admin/config/my-module/settings" defaults: _form: '\Drupal\my_module\Form\SettingsForm' - _title: 'My Module Settings' + _title: "My Module Settings" requirements: - _permission: 'administer site configuration' + _permission: "administer site configuration" my_module.custom_access: - path: '/my-module/custom/{node}' + path: "/my-module/custom/{node}" defaults: _controller: '\Drupal\my_module\Controller\MyController::customPage' _title_callback: '\Drupal\my_module\Controller\MyController::pageTitle' @@ -663,6 +687,7 @@ my_module.custom_access: ``` **Controller**: + ```php namespace Drupal\my_module\Controller; @@ -705,6 +730,7 @@ class MyController extends ControllerBase { ``` **Custom access checker**: + ```php namespace Drupal\my_module\Access; @@ -723,16 +749,18 @@ class MyAccessChecker implements AccessInterface { ``` Register in `my_module.services.yml`: + ```yaml - my_module.access_checker: - class: Drupal\my_module\Access\MyAccessChecker - tags: - - { name: access_check } +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 @@ -743,6 +771,7 @@ Register in `my_module.services.yml`: - **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 @@ -755,6 +784,7 @@ Register in `my_module.services.yml`: - **Entity loading**: Load multiple entities at once with `loadMultiple()` instead of individual loads **Render array with caching**: + ```php $build = [ '#theme' => 'item_list', @@ -769,6 +799,7 @@ $build = [ ``` **Redis configuration for Lagoon** (in `settings.php`): + ```php // Redis configuration for Lagoon if (getenv('LAGOON')) { @@ -781,6 +812,7 @@ if (getenv('LAGOON')) { ``` ### 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 @@ -822,6 +854,7 @@ These are common mistakes that an AI agent must avoid: ## Testing & Quality Assurance ### PHPUnit Testing Framework + Aim for ≥ 80% code coverage. Drupal provides multiple test types: ```bash @@ -843,6 +876,7 @@ SIMPLETEST_DB=sqlite://localhost/tmp.sqlite vendor/bin/phpunit ``` ### Unit Test Example + ```php // tests/src/Unit/MyServiceTest.php namespace Drupal\Tests\my_module\Unit; @@ -871,6 +905,7 @@ class MyServiceTest extends UnitTestCase { ``` ### Kernel Test Example + ```php // tests/src/Kernel/MyModuleKernelTest.php namespace Drupal\Tests\my_module\Kernel; @@ -908,6 +943,7 @@ class MyModuleKernelTest extends KernelTestBase { ``` ### Functional Test Example + ```php // tests/src/Functional/MyModuleFunctionalTest.php namespace Drupal\Tests\my_module\Functional; @@ -945,6 +981,7 @@ class MyModuleFunctionalTest extends BrowserTestBase { ``` ### Code Quality Tools + ```bash # Static analysis vendor/bin/phpstan analyse @@ -959,6 +996,7 @@ vendor/bin/phpunit --group accessibility ``` ### Before Submitting Code + ```bash # Quality checklist vendor/bin/phpcs --standard=Drupal . @@ -970,6 +1008,7 @@ drush updatedb ## Lagoon Development Workflow ### Project Structure + - **Modules** → `web/modules/custom/` - **Themes** → `web/themes/custom/` - **Configuration** → Export with `drush config:export` @@ -978,6 +1017,7 @@ drush updatedb - **Docker Compose** → `docker-compose.yml` in project root ### Deployment Workflow + ```bash # Feature development workflow git checkout -b feature/my-feature @@ -999,6 +1039,7 @@ git push origin main ``` ### Remote Drush Commands + ```bash # Run Drush on a remote Lagoon environment drush @lagoon.main status @@ -1017,6 +1058,7 @@ 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 @@ -1029,6 +1071,7 @@ lagoon-sync sync files -p -e main -t local Prefer EventSubscribers over hooks for many use cases. They are more testable and follow Symfony conventions. **EventSubscriber example**: + ```php namespace Drupal\my_module\EventSubscriber; @@ -1057,44 +1100,48 @@ class MyEventSubscriber implements EventSubscriberInterface { ``` Register in `my_module.services.yml`: + ```yaml - my_module.event_subscriber: - class: Drupal\my_module\EventSubscriber\MyEventSubscriber - tags: - - { name: event_subscriber } +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' + label: "My Module settings" mapping: api_key: type: string - label: 'API Key' + label: "API Key" max_items: type: integer - label: 'Maximum items' + label: "Maximum items" enabled_types: type: sequence - label: 'Enabled content types' + label: "Enabled content types" sequence: type: string - label: 'Content type' + label: "Content type" ``` **Config install** (`config/install/my_module.settings.yml`): + ```yaml -api_key: '' +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'); @@ -1104,6 +1151,7 @@ $value = \Drupal::config('my_module.settings')->get('api_key'); ``` **Config workflow**: + ```bash # Export all configuration drush config:export @@ -1208,6 +1256,7 @@ class MyQueueWorker extends QueueWorkerBase implements ContainerFactoryPluginInt ``` ### 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 @@ -1289,23 +1338,24 @@ drush cr ### JavaScript & Frontend **Drupal behaviors**: + ```javascript (function (Drupal, drupalSettings) { - 'use strict'; + "use strict"; Drupal.behaviors.myModuleBehavior = { attach: function (context, settings) { - const elements = context.querySelectorAll('.my-element'); + const elements = context.querySelectorAll(".my-element"); elements.forEach(function (element) { - element.addEventListener('click', handleClick); + element.addEventListener("click", handleClick); }); }, detach: function (context, settings, trigger) { - const elements = context.querySelectorAll('.my-element'); + const elements = context.querySelectorAll(".my-element"); elements.forEach(function (element) { - element.removeEventListener('click', handleClick); + element.removeEventListener("click", handleClick); }); - } + }, }; })(Drupal, drupalSettings); ``` @@ -1313,6 +1363,7 @@ drush cr ## Troubleshooting ### Lagoon Deployment Issues + ```bash # Check deployment status lagoon get environment --project --environment @@ -1328,6 +1379,7 @@ drush @lagoon. status ``` ### Database Sync Issues + ```bash # If lagoon-sync fails, try Drush drush sql:sync @lagoon.main @self @@ -1337,6 +1389,7 @@ drush cr ``` ### Performance Issues + ```bash # Check remote cache settings drush @lagoon.main config:get system.performance @@ -1351,18 +1404,21 @@ drush @lagoon.main php:eval "var_dump(\Drupal::service('cache.default')->get('te ## 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 diff --git a/README.md b/README.md index b9f4e30..61c72c7 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ 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. +> **⚠️ 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 @@ -19,6 +18,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) @@ -28,6 +28,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 @@ -37,6 +38,7 @@ This repository contains specialized AGENTS.md files designed for AI coding agen - **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 @@ -47,17 +49,18 @@ This repository contains specialized AGENTS.md files designed for AI coding agen ## 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 | +| 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** (with code examples) + - Services & Dependency Injection - Entity API & Queries - Plugin System @@ -76,22 +79,26 @@ Each AGENTS.md file contains: - Content Moderation & Workflows ### 🛡️ **Security & Performance** + - Security best practices - Performance optimization - Caching strategies (render, Varnish, Redis) - Lazy builders and placeholder strategies ### 🧪 **Testing & Quality** (with code examples) + - PHPUnit testing framework - 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** + - Module scaffolding template with full file structure - Environment-specific commands - Debugging tools and tables @@ -100,11 +107,9 @@ Each AGENTS.md file contains: ## 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: @@ -125,6 +130,7 @@ Each AGENTS.md file contains: ## Key Features ### ✅ **What We Provide** + - Comprehensive Drupal development patterns with concrete code examples - Environment-specific instructions (DDEV, Vanilla, Lagoon) - Security and performance guidelines @@ -136,6 +142,7 @@ Each AGENTS.md file contains: - Troubleshooting guides ### ❌ **What We Don't Include** + - Infrastructure setup tutorials - Server configuration details - Basic Drupal installation guides @@ -145,6 +152,7 @@ Each AGENTS.md file contains: ## 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 diff --git a/Vanilla/AGENTS.md b/Vanilla/AGENTS.md index fffd06c..76a2337 100644 --- a/Vanilla/AGENTS.md +++ b/Vanilla/AGENTS.md @@ -19,6 +19,7 @@ - [Additional Resources](#additional-resources) ## Project Overview + - **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.3+, MySQL/PostgreSQL, Apache/Nginx @@ -26,6 +27,7 @@ - **Important**: Always run commands from project root unless specified ## Prerequisites + ```bash # System requirements PHP 8.3+ with required extensions (gd, xml, mbstring, json, pdo, curl, zip) @@ -110,10 +112,11 @@ modules/custom/my_module/ ### Minimal Module Files **my_module.info.yml**: + ```yaml -name: 'My Module' +name: "My Module" type: module -description: 'Custom module description.' +description: "Custom module description." core_version_requirement: ^10 || ^11 package: Custom dependencies: @@ -122,6 +125,7 @@ dependencies: ``` **composer.json** (for PSR-4 autoloading in tests): + ```json { "name": "drupal/my_module", @@ -141,6 +145,7 @@ dependencies: ``` ## Code Style and Standards + Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and PHPCS for enforcement. - **PHP**: @@ -154,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/ @@ -167,17 +173,19 @@ Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and ### 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'] + 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; @@ -213,6 +221,7 @@ class MyController extends ControllerBase { ### Entity API & Queries **Loading entities**: + ```php // Single entity $node = \Drupal::entityTypeManager()->getStorage('node')->load(123); @@ -228,6 +237,7 @@ $nodes = \Drupal::entityTypeManager()->getStorage('node')->loadByProperties([ ``` **Entity queries** (always prefer over raw SQL): + ```php use Drupal\Core\Entity\Query\QueryInterface; @@ -245,6 +255,7 @@ $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($ids); ``` **Creating entities**: + ```php $node = \Drupal::entityTypeManager()->getStorage('node')->create([ 'type' => 'article', @@ -260,6 +271,7 @@ $node->save(); ``` **Field access**: Use entity field API instead of direct property access: + ```php // Correct $node->get('field_my_field')->value; @@ -272,6 +284,7 @@ $node->field_my_field->value; // Magic __get — works but less explicit ### Plugin System **Block plugin example**: + ```php namespace Drupal\my_module\Plugin\Block; @@ -373,6 +386,7 @@ function my_module_cron(): void { ### Forms API **Simple form**: + ```php namespace Drupal\my_module\Form; @@ -443,6 +457,7 @@ class CustomForm extends FormBase { ``` **Configuration form**: + ```php namespace Drupal\my_module\Form; @@ -494,26 +509,27 @@ class SettingsForm extends ConfigFormBase { ### Routes & Controllers **Route definition** (`my_module.routing.yml`): + ```yaml my_module.content: - path: '/my-module/{node}' + path: "/my-module/{node}" defaults: _controller: '\Drupal\my_module\Controller\MyController::content' - _title: 'My Module Page' + _title: "My Module Page" requirements: - _permission: 'access content' + _permission: "access content" node: \d+ my_module.settings: - path: '/admin/config/my-module/settings' + path: "/admin/config/my-module/settings" defaults: _form: '\Drupal\my_module\Form\SettingsForm' - _title: 'My Module Settings' + _title: "My Module Settings" requirements: - _permission: 'administer site configuration' + _permission: "administer site configuration" my_module.custom_access: - path: '/my-module/custom/{node}' + path: "/my-module/custom/{node}" defaults: _controller: '\Drupal\my_module\Controller\MyController::customPage' _title_callback: '\Drupal\my_module\Controller\MyController::pageTitle' @@ -522,6 +538,7 @@ my_module.custom_access: ``` **Controller**: + ```php namespace Drupal\my_module\Controller; @@ -564,6 +581,7 @@ class MyController extends ControllerBase { ``` **Custom access checker**: + ```php namespace Drupal\my_module\Access; @@ -582,16 +600,18 @@ class MyAccessChecker implements AccessInterface { ``` Register in `my_module.services.yml`: + ```yaml - my_module.access_checker: - class: Drupal\my_module\Access\MyAccessChecker - tags: - - { name: access_check } +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 @@ -602,6 +622,7 @@ Register in `my_module.services.yml`: - **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 @@ -613,6 +634,7 @@ Register in `my_module.services.yml`: - **Entity loading**: Load multiple entities at once with `loadMultiple()` instead of individual loads **Render array with caching**: + ```php $build = [ '#theme' => 'item_list', @@ -627,6 +649,7 @@ $build = [ ``` **Lazy builder for expensive operations**: + ```php $build['expensive_content'] = [ '#lazy_builder' => [ @@ -638,6 +661,7 @@ $build['expensive_content'] = [ ``` ### Caching Strategies + - **Render cache**: Cache complex markup with proper tags/contexts - **Dynamic page cache**: Automatically handles cacheability for anonymous users - **Internal page cache**: Serves full cached pages for anonymous users @@ -645,6 +669,7 @@ $build['expensive_content'] = [ - **Redis/Memcache**: Configure for distributed caching in production ### Server Optimization + ```bash # PHP configuration (php.ini) memory_limit = 256M @@ -692,6 +717,7 @@ These are common mistakes that an AI agent must avoid: ## Testing & Quality Assurance ### PHPUnit Testing Framework + Aim for ≥ 80% code coverage. Drupal provides multiple test types: ```bash @@ -713,6 +739,7 @@ SIMPLETEST_DB=sqlite://localhost/tmp.sqlite vendor/bin/phpunit ``` ### Unit Test Example + ```php // tests/src/Unit/MyServiceTest.php namespace Drupal\Tests\my_module\Unit; @@ -741,6 +768,7 @@ class MyServiceTest extends UnitTestCase { ``` ### Kernel Test Example + ```php // tests/src/Kernel/MyModuleKernelTest.php namespace Drupal\Tests\my_module\Kernel; @@ -778,6 +806,7 @@ class MyModuleKernelTest extends KernelTestBase { ``` ### Functional Test Example + ```php // tests/src/Functional/MyModuleFunctionalTest.php namespace Drupal\Tests\my_module\Functional; @@ -818,6 +847,7 @@ class MyModuleFunctionalTest extends BrowserTestBase { ``` ### Code Quality Tools + ```bash # Static analysis (add to composer require) vendor/bin/phpstan analyse # PHPStan analysis @@ -832,6 +862,7 @@ vendor/bin/phpunit --group accessibility # Accessibility tests ``` ### JavaScript Testing + ```bash # Install JavaScript dependencies npm install @@ -842,6 +873,7 @@ npm run test:a11y # Accessibility tests ``` ### Before Submitting Code + ```bash # Quality checklist vendor/bin/phpcs --standard=Drupal . # Code style @@ -853,12 +885,14 @@ 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 @@ -878,47 +912,53 @@ drush updatedb # Run database updates ### Debugging Tools #### Core Debugging & Information Commands -| Command | Purpose | -|----------------------------------|-------------------------------------------------------------------------| -| `drush status` | Shows Drupal root, site path, database connection, Drush version | + +| 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 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 | -| `drush cache:get :` | Retrieve a specific cache item | -| `drush cache:clear ` | Clear only one cache bin | + +| 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 | -| `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 | + +| 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 | + +| 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 | +| `drush theme:debug` | Lists all theme suggestions | #### Database & Entity Debugging -| 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 | + +| 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 + ```bash drush sql:query --db-prefix # See queries with table prefixes expanded drush twig:debug # Turn Twig debugging on/off @@ -926,6 +966,7 @@ drush state:get/set/delete # Inspect or override Drupal state values ``` ### Performance Profiling + ```bash # Performance analysis drush cr # Rebuild caches @@ -937,6 +978,7 @@ 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 @@ -949,6 +991,7 @@ drush site:status # System status check Prefer EventSubscribers over hooks for many use cases. They are more testable and follow Symfony conventions. **EventSubscriber example**: + ```php namespace Drupal\my_module\EventSubscriber; @@ -978,11 +1021,12 @@ class MyEventSubscriber implements EventSubscriberInterface { ``` Register in `my_module.services.yml`: + ```yaml - my_module.event_subscriber: - class: Drupal\my_module\EventSubscriber\MyEventSubscriber - tags: - - { name: event_subscriber } +my_module.event_subscriber: + class: Drupal\my_module\EventSubscriber\MyEventSubscriber + tags: + - { name: event_subscriber } ``` **Drupal-specific events**: `HookEventDispatcher` module provides events for most Drupal hooks. Core events include entity events and kernel events. @@ -990,34 +1034,37 @@ Register in `my_module.services.yml`: ### Configuration Management **Config schema** (`config/schema/my_module.schema.yml`): + ```yaml my_module.settings: type: config_object - label: 'My Module settings' + label: "My Module settings" mapping: api_key: type: string - label: 'API Key' + label: "API Key" max_items: type: integer - label: 'Maximum items' + label: "Maximum items" enabled_types: type: sequence - label: 'Enabled content types' + label: "Enabled content types" sequence: type: string - label: 'Content type' + label: "Content type" ``` **Config install** (`config/install/my_module.settings.yml`): + ```yaml -api_key: '' +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'); @@ -1027,6 +1074,7 @@ $value = \Drupal::config('my_module.settings')->get('api_key'); ``` **Config workflow**: + ```bash # Export all configuration drush config:export @@ -1137,6 +1185,7 @@ class MyQueueWorker extends QueueWorkerBase implements ContainerFactoryPluginInt ``` **Adding items to the queue**: + ```php \Drupal::queue('my_module_processor')->createItem(['type' => 'cleanup', 'node_id' => 123]); ``` @@ -1146,6 +1195,7 @@ class MyQueueWorker extends QueueWorkerBase implements ContainerFactoryPluginInt - **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 @@ -1205,10 +1255,8 @@ source: keys: - id column_names: - - - id: [id, 'Unique ID'] - - - title: [title, 'Title'] + - id: [id, "Unique ID"] + - title: [title, "Title"] # Process plugin process: @@ -1231,6 +1279,7 @@ destination: ``` **Custom process plugin**: + ```php namespace Drupal\my_module\Plugin\migrate\process; @@ -1279,6 +1328,7 @@ 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 @@ -1288,26 +1338,27 @@ drush cr ### JavaScript & Frontend **Drupal behaviors** (not jQuery document.ready): + ```javascript // js/my-module.js (function (Drupal, drupalSettings) { - 'use strict'; + "use strict"; Drupal.behaviors.myModuleBehavior = { attach: function (context, settings) { // Run on every page load and AJAX response. - const elements = context.querySelectorAll('.my-element'); + const elements = context.querySelectorAll(".my-element"); elements.forEach(function (element) { - element.addEventListener('click', handleClick); + element.addEventListener("click", handleClick); }); }, detach: function (context, settings, trigger) { // Clean up when content is removed (AJAX, etc.). - const elements = context.querySelectorAll('.my-element'); + const elements = context.querySelectorAll(".my-element"); elements.forEach(function (element) { - element.removeEventListener('click', handleClick); + element.removeEventListener("click", handleClick); }); - } + }, }; function handleClick(event) { @@ -1317,6 +1368,7 @@ drush cr ``` **Library definition** (`my_module.libraries.yml`): + ```yaml my_module.styles: version: VERSION @@ -1331,6 +1383,7 @@ my_module.styles: ``` **Attaching libraries**: + ```php // In render array $build['#attached']['library'][] = 'my_module/my_module.styles'; @@ -1363,6 +1416,7 @@ $node->save(); ## Troubleshooting Common Issues ### Installation Problems + ```bash # Composer memory issues php -d memory_limit=-1 /usr/local/bin/composer install @@ -1380,6 +1434,7 @@ 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" @@ -1389,6 +1444,7 @@ drush config:get system.performance ``` ### Module/Theme Development Issues + ```bash drush cr @@ -1404,6 +1460,7 @@ drush watchdog:show --type=cron ``` ### Testing Issues + ```bash # PHPUnit configuration — ensure phpunit.xml.dist exists and is configured cp web/core/phpunit.xml.dist phpunit.xml @@ -1419,6 +1476,7 @@ cp web/core/phpunit.xml.dist phpunit.xml ## 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 @@ -1427,6 +1485,7 @@ cp web/core/phpunit.xml.dist phpunit.xml - **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 index 1329bca..d4a03dd 100644 --- a/experimental/.kb/00-index.md +++ b/experimental/.kb/00-index.md @@ -1,9 +1,9 @@ --- 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. + 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] --- @@ -13,28 +13,28 @@ This knowledge base contains focused, self-contained guides for Drupal 10.x/11.x ## 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) | +| 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 diff --git a/experimental/.kb/01-project-overview.md b/experimental/.kb/01-project-overview.md index ea64270..e89b848 100644 --- a/experimental/.kb/01-project-overview.md +++ b/experimental/.kb/01-project-overview.md @@ -1,15 +1,16 @@ --- 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. + 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+ @@ -19,6 +20,7 @@ tags: [overview, setup, prerequisites, stack] - **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` @@ -26,6 +28,7 @@ tags: [overview, setup, prerequisites, stack] - 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) @@ -33,6 +36,7 @@ tags: [overview, setup, prerequisites, stack] - 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+ @@ -42,6 +46,7 @@ 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 index 8560b1c..2809103 100644 --- a/experimental/.kb/02-code-standards.md +++ b/experimental/.kb/02-code-standards.md @@ -1,9 +1,9 @@ --- 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. + 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] --- @@ -12,6 +12,7 @@ tags: [standards, php, yaml, twig, linting, phpcs, code-style] 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 @@ -21,16 +22,19 @@ Adhere to Drupal coding standards (PSR-12 with Drupal extensions). Use Coder and - **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/ @@ -43,6 +47,7 @@ vendor/bin/phpcs --standard=Drupal --fix src/ ``` ## Static Analysis + ```bash vendor/bin/phpstan analyse # PHPStan vendor/bin/psalm # Psalm @@ -53,5 +58,6 @@ 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 index 456095c..9360673 100644 --- a/experimental/.kb/03-module-scaffolding.md +++ b/experimental/.kb/03-module-scaffolding.md @@ -1,8 +1,9 @@ --- 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. + 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] --- @@ -53,10 +54,11 @@ modules/custom/my_module/ ## Minimal Required Files **my_module.info.yml**: + ```yaml -name: 'My Module' +name: "My Module" type: module -description: 'Custom module description.' +description: "Custom module description." core_version_requirement: ^10 || ^11 package: Custom dependencies: @@ -65,6 +67,7 @@ dependencies: ``` **composer.json** (PSR-4 autoloading for tests): + ```json { "name": "drupal/my_module", @@ -84,6 +87,7 @@ dependencies: ``` ## 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 diff --git a/experimental/.kb/04-services-di.md b/experimental/.kb/04-services-di.md index b75ce03..13f7c1f 100644 --- a/experimental/.kb/04-services-di.md +++ b/experimental/.kb/04-services-di.md @@ -1,9 +1,9 @@ --- 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. + 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] --- @@ -12,11 +12,12 @@ tags: [services, dependency-injection, di, container, service-container] ## 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'] + arguments: ["@entity_type.manager", "@logger.factory", "@config.factory"] tags: - { name: backend_overridable } ``` @@ -24,6 +25,7 @@ services: ## Use Dependency Injection ### In Controllers + ```php namespace Drupal\my_module\Controller; @@ -51,6 +53,7 @@ class MyController extends ControllerBase { ``` ### In Plugins + ```php use Drupal\Core\Plugin\ContainerFactoryPluginInterface; @@ -77,31 +80,35 @@ class MyBlock extends BlockBase implements ContainerFactoryPluginInterface { ``` ## Common Core Services -| Service ID | Purpose | -|---|---| + +| 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) | +| `@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 index da42ab2..1adc349 100644 --- a/experimental/.kb/05-entity-api.md +++ b/experimental/.kb/05-entity-api.md @@ -1,9 +1,9 @@ --- 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. + 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] --- @@ -85,6 +85,7 @@ $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 index 4aee4b5..81caccf 100644 --- a/experimental/.kb/06-plugins.md +++ b/experimental/.kb/06-plugins.md @@ -1,14 +1,16 @@ --- title: Plugin System description: > - Drupal's plugin system: annotation-based discovery, base classes, and the - ContainerFactoryPluginInterface pattern. Includes a complete Block plugin example. + 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) @@ -74,6 +76,7 @@ class MyCustomBlock extends BlockBase implements ContainerFactoryPluginInterface - **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 index 3b8d088..62e86f2 100644 --- a/experimental/.kb/07-hooks.md +++ b/experimental/.kb/07-hooks.md @@ -1,9 +1,9 @@ --- 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. + 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] --- @@ -14,6 +14,7 @@ Hooks are implemented in `modulename.module` files. Keep them thin — delegate ## Common Hooks with Examples ### hook_form_alter() + ```php /** * Implements hook_form_alter(). @@ -27,6 +28,7 @@ function my_module_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form ``` ### hook_theme() + ```php /** * Implements hook_theme(). @@ -45,6 +47,7 @@ function my_module_theme($existing, $type, $theme, $path): array { ``` ### hook_entity_presave() + ```php /** * Implements hook_entity_presave(). @@ -57,6 +60,7 @@ function my_module_entity_presave(\Drupal\Core\Entity\EntityInterface $entity): ``` ### hook_cron() + ```php /** * Implements hook_cron(). @@ -67,12 +71,14 @@ function my_module_cron(): void { ``` ## 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 @@ -80,6 +86,7 @@ function my_module_cron(): void { - `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 index 621b7fe..3fe12cc 100644 --- a/experimental/.kb/08-forms.md +++ b/experimental/.kb/08-forms.md @@ -1,9 +1,9 @@ --- 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. + 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] --- @@ -131,6 +131,7 @@ class SettingsForm extends ConfigFormBase { ``` ## AJAX Forms Quick Reference + - Add `#ajax` property to any form element - Callback: `'::methodName'` syntax - Wrapper: target element ID for replacement @@ -139,6 +140,7 @@ class SettingsForm extends ConfigFormBase { - 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 index 54d60da..91017c6 100644 --- a/experimental/.kb/09-routes-controllers.md +++ b/experimental/.kb/09-routes-controllers.md @@ -1,8 +1,9 @@ --- title: Routes & Controllers description: > - Drupal routing system, controller classes, route parameters, and custom - access checkers. Includes complete YAML route definitions and PHP examples. + 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] --- @@ -13,26 +14,26 @@ tags: [routing, controllers, routes, access-checker, permissions] ```yaml # Basic controller route with parameter upcasting my_module.content: - path: '/my-module/{node}' + path: "/my-module/{node}" defaults: _controller: '\Drupal\my_module\Controller\MyController::content' - _title: 'My Module Page' + _title: "My Module Page" requirements: - _permission: 'access content' + _permission: "access content" node: \d+ # Form route my_module.settings: - path: '/admin/config/my-module/settings' + path: "/admin/config/my-module/settings" defaults: _form: '\Drupal\my_module\Form\SettingsForm' - _title: 'My Module Settings' + _title: "My Module Settings" requirements: - _permission: 'administer site configuration' + _permission: "administer site configuration" # Route with custom access checker my_module.custom_access: - path: '/my-module/custom/{node}' + path: "/my-module/custom/{node}" defaults: _controller: '\Drupal\my_module\Controller\MyController::customPage' _title_callback: '\Drupal\my_module\Controller\MyController::pageTitle' @@ -103,15 +104,17 @@ class MyAccessChecker implements AccessInterface { ``` 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 } +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 @@ -119,6 +122,7 @@ Register as a service with the `access_check` tag: - `_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 index d1e37f7..16d8e05 100644 --- a/experimental/.kb/10-security.md +++ b/experimental/.kb/10-security.md @@ -1,26 +1,31 @@ --- title: Security Best Practices description: > - Drupal security requirements: input sanitization, XSS prevention, CSRF - protection, SQL injection avoidance, and secure coding patterns. + 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", @@ -29,6 +34,7 @@ tags: [security, xss, csrf, sql-injection, sanitization, permissions] ``` ## XSS Prevention + ```php // ✅ Correct — safe $build['output'] = ['#plain_text' => $user_input]; @@ -38,6 +44,7 @@ $build['output'] = ['#markup' => $user_input]; ``` In Twig: + ```twig {# ✅ Correct — auto-escaped #} {{ user_input }} @@ -47,20 +54,24 @@ In Twig: ``` ## 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 index ea6c9c0..ac56302 100644 --- a/experimental/.kb/11-caching-performance.md +++ b/experimental/.kb/11-caching-performance.md @@ -1,9 +1,9 @@ --- 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. + 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] --- @@ -25,7 +25,9 @@ $build = [ ``` ## Cache Tags + Invalidate when the underlying data changes: + ```php // Entity-specific ['node:123', 'node:456'] @@ -38,7 +40,9 @@ Invalidate when the underlying data changes: ``` ## Cache Contexts + Vary output by: + ```php ['user.roles'] // Different per role ['user.permissions'] // Different per permission set @@ -48,6 +52,7 @@ Vary output by: ``` ## Lazy Builders (for expensive operations) + ```php $build['expensive'] = [ '#lazy_builder' => [ @@ -59,21 +64,24 @@ $build['expensive'] = [ ``` ## 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 | + +| 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 @@ -84,6 +92,7 @@ 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 index c29558c..74fac92 100644 --- a/experimental/.kb/12-anti-patterns.md +++ b/experimental/.kb/12-anti-patterns.md @@ -1,9 +1,9 @@ --- 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. + 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] --- diff --git a/experimental/.kb/13-testing.md b/experimental/.kb/13-testing.md index d43a5f5..8a08925 100644 --- a/experimental/.kb/13-testing.md +++ b/experimental/.kb/13-testing.md @@ -1,14 +1,16 @@ --- title: Testing & Quality Assurance description: > - Drupal testing with PHPUnit: Unit, Kernel, and Functional test examples. - Covers test base classes, configuration, and code quality tools. + 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/ @@ -25,6 +27,7 @@ vendor/bin/phpunit modules/custom/my_module/tests/src/Unit/ ``` ## Unit Test (fastest — no Drupal bootstrap) + ```php namespace Drupal\Tests\my_module\Unit; @@ -50,11 +53,13 @@ class MyServiceTest extends UnitTestCase { } } ``` + - **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; @@ -86,10 +91,12 @@ class MyModuleKernelTest extends KernelTestBase { } } ``` + - **Location**: `tests/src/Kernel/` - **Base class**: `Drupal\KernelTests\KernelTestBase` ## Functional Test (full browser simulation) + ```php namespace Drupal\Tests\my_module\Functional; @@ -115,10 +122,12 @@ class MyModuleFunctionalTest extends BrowserTestBase { } } ``` + - **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 @@ -127,5 +136,6 @@ 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 index 7bd3bb3..a90d60d 100644 --- a/experimental/.kb/14-events.md +++ b/experimental/.kb/14-events.md @@ -1,8 +1,9 @@ --- title: Events & EventSubscribers description: > - Symfony EventDispatcher in Drupal: replacing hooks with event subscribers - for better testability. Includes complete example with service registration. + 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] --- @@ -43,27 +44,30 @@ class MyEventSubscriber implements EventSubscriberInterface { ```yaml # my_module.services.yml - my_module.event_subscriber: - class: Drupal\my_module\EventSubscriber\MyEventSubscriber - arguments: ['@current_user', '@config.factory'] - tags: - - { name: event_subscriber } +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 | + +| 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 index 5807e0e..3663e55 100644 --- a/experimental/.kb/15-configuration.md +++ b/experimental/.kb/15-configuration.md @@ -1,46 +1,51 @@ --- 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. + 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' + label: "My Module settings" mapping: api_key: type: string - label: 'API Key' + label: "API Key" max_items: type: integer - label: 'Maximum items' + label: "Maximum items" enabled_types: type: sequence - label: 'Enabled content types' + label: "Enabled content types" sequence: type: string - label: 'Content type' + label: "Content type" ``` ## Install vs Optional Config + - **`config/install/`** — Installed when module is enabled (required) + ```yaml # config/install/my_module.settings.yml - api_key: '' + 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'); @@ -50,6 +55,7 @@ $value = \Drupal::config('my_module.settings')->get('api_key'); ``` ## Writing Config + ```php $this->configFactory->getEditable('my_module.settings') ->set('api_key', 'new-value') @@ -57,6 +63,7 @@ $this->configFactory->getEditable('my_module.settings') ``` ## Config Override (settings.php) + ```php // Environment-specific overrides (not exported) $config['system.performance']['css']['preprocess'] = FALSE; @@ -64,6 +71,7 @@ $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 @@ -74,11 +82,14 @@ 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 index 895c702..e45eb23 100644 --- a/experimental/.kb/16-batch-queue.md +++ b/experimental/.kb/16-batch-queue.md @@ -1,9 +1,9 @@ --- 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. + 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] --- @@ -68,6 +68,7 @@ function my_module_batch_finished(bool $success, array $results, array $operatio ## Queue API (background processing) ### QueueWorker Plugin + ```php namespace Drupal\my_module\Plugin\QueueWorker; @@ -98,6 +99,7 @@ class MyQueueWorker extends QueueWorkerBase implements ContainerFactoryPluginInt ``` ### Adding Items to Queue + ```php \Drupal::queue('my_module_processor')->createItem(['type' => 'cleanup', 'node_id' => 123]); ``` @@ -107,5 +109,6 @@ class MyQueueWorker extends QueueWorkerBase implements ContainerFactoryPluginInt - 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 index 21c58d5..606ad96 100644 --- a/experimental/.kb/17-render-api.md +++ b/experimental/.kb/17-render-api.md @@ -1,8 +1,9 @@ --- 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. + 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] --- @@ -42,23 +43,24 @@ $build = [ ## 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 | +| 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'], @@ -69,6 +71,7 @@ $build['#attached'] = [ ``` ## #lazy_builder — Deferred Rendering + ```php $build['expensive'] = [ '#lazy_builder' => [ @@ -80,9 +83,11 @@ $build['expensive'] = [ ``` ## 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 index c47254a..66c7f42 100644 --- a/experimental/.kb/18-migration.md +++ b/experimental/.kb/18-migration.md @@ -1,8 +1,9 @@ --- title: Migration API description: > - Drupal's Migration API: source, process, and destination plugins with YAML - definitions and custom process plugin example. + 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] --- @@ -13,7 +14,7 @@ tags: [migration, migrate, migrate-api, process-plugin, source, destination] ```yaml # migrations/my_migration.yml id: my_migration -label: 'My Custom Migration' +label: "My Custom Migration" source: plugin: csv path: /path/to/data.csv @@ -21,10 +22,8 @@ source: keys: - id column_names: - - - id: [id, 'Unique ID'] - - - title: [title, 'Title'] + - id: [id, "Unique ID"] + - title: [title, "Title"] process: title: title @@ -72,6 +71,7 @@ class MyCustomProcess extends ProcessPluginBase { ``` ## Common Source Plugins + - `csv` — CSV file (requires `migrate_source_csv`) - `d7_node`, `d7_user` — Drupal 7 migrations - `sql` — Direct database queries @@ -79,6 +79,7 @@ class MyCustomProcess extends ProcessPluginBase { - `embedded_data` — Inline data for testing ## Common Process Plugins + - `get` — Pass through value - `default_value` — Set default - `callback` — PHP function callback @@ -87,12 +88,14 @@ class MyCustomProcess extends ProcessPluginBase { - `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 @@ -101,5 +104,6 @@ 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 index 9e3aa4c..b9e4ee1 100644 --- a/experimental/.kb/19-composer.md +++ b/experimental/.kb/19-composer.md @@ -1,14 +1,16 @@ --- title: Composer Management description: > - Managing Drupal dependencies with Composer: adding modules, applying patches, - updating core, and composer.json best practices. + 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 @@ -29,7 +31,9 @@ drush cr ``` ## Applying Patches + Add the `composer-patches` plugin, then add patches to `composer.json`: + ```json { "extra": { @@ -43,16 +47,19 @@ Add the `composer-patches` plugin, then add patches to `composer.json`: ``` ## 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 @@ -63,5 +70,6 @@ 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 index c75947a..77d779f 100644 --- a/experimental/.kb/20-javascript.md +++ b/experimental/.kb/20-javascript.md @@ -1,8 +1,9 @@ --- title: JavaScript & Frontend description: > - Drupal's JavaScript system: Drupal behaviors, library definitions, - drupalSettings, and attaching assets to render arrays and Twig templates. + 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] --- @@ -13,23 +14,23 @@ tags: [javascript, js, drupal-behaviors, libraries, drupal-settings, frontend] ```javascript // js/my-module.js (function (Drupal, drupalSettings) { - 'use strict'; + "use strict"; Drupal.behaviors.myModuleBehavior = { attach: function (context, settings) { // Runs on every page load AND AJAX response. - const elements = context.querySelectorAll('.my-element'); + const elements = context.querySelectorAll(".my-element"); elements.forEach(function (element) { - element.addEventListener('click', handleClick); + element.addEventListener("click", handleClick); }); }, detach: function (context, settings, trigger) { // Clean up when content is removed (AJAX, etc.). - const elements = context.querySelectorAll('.my-element'); + const elements = context.querySelectorAll(".my-element"); elements.forEach(function (element) { - element.removeEventListener('click', handleClick); + element.removeEventListener("click", handleClick); }); - } + }, }; function handleClick(event) { @@ -39,12 +40,14 @@ tags: [javascript, js, drupal-behaviors, libraries, drupal-settings, frontend] ``` **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 @@ -73,6 +76,7 @@ $build['#attached']['library'][] = 'my_module/my_module.styles'; ``` ## Passing Data to JS (drupalSettings) + ```php $build['#attached']['drupalSettings']['my_module'] = [ 'endpoint' => '/api/items', @@ -83,5 +87,6 @@ $build['#attached']['drupalSettings']['my_module'] = [ 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 index f29f3aa..d6161fb 100644 --- a/experimental/.kb/21-workflow.md +++ b/experimental/.kb/21-workflow.md @@ -1,15 +1,16 @@ --- title: Development Workflow & Debugging description: > - Essential Drush commands for development, debugging tables for cache, - config, modules, entities, and performance profiling. Version control - workflow conventions. + 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 @@ -22,50 +23,51 @@ 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 | +| 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 | +| 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 | +| 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 | +| 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 | +| 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 @@ -74,17 +76,20 @@ drush config:get system.performance # Check perf settings ``` ## 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 index 3c38f41..a63701d 100644 --- a/experimental/.kb/22-troubleshooting.md +++ b/experimental/.kb/22-troubleshooting.md @@ -1,14 +1,16 @@ --- title: Troubleshooting Common Issues description: > - Solutions to common Drupal development problems: installation failures, - performance issues, module/theme problems, and testing configuration. + 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 @@ -26,6 +28,7 @@ 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" @@ -35,6 +38,7 @@ drush config:get system.performance ``` ## Module/Theme Development Issues + ```bash # Most issues are solved by clearing caches drush cr @@ -53,6 +57,7 @@ drush watchdog:show --type=cron ``` ## Testing Issues + ```bash # PHPUnit not configured cp web/core/phpunit.xml.dist phpunit.xml @@ -68,6 +73,7 @@ cp web/core/phpunit.xml.dist phpunit.xml ``` ## White Screen of Death (WSOD) + ```bash # Check PHP error logs tail -f /var/log/apache2/error.log # Apache @@ -81,6 +87,7 @@ 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 @@ -88,6 +95,7 @@ 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 index 49b6ca1..9e234ea 100644 --- a/experimental/AGENTS.md +++ b/experimental/AGENTS.md @@ -3,11 +3,13 @@ **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` @@ -15,6 +17,7 @@ - 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 @@ -25,33 +28,34 @@ 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 | +| 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 diff --git a/package.json b/package.json new file mode 100644 index 0000000..8845139 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "drupal-agents-md", + "private": true, + "description": "AI Agent Development Guides for Drupal", + "scripts": { + "lint": "markdownlint '**/*.md'", + "lint:fix": "markdownlint --fix '**/*.md' && prettier --write '**/*.md'", + "format": "prettier --write '**/*.md'", + "check": "markdownlint '**/*.md' && prettier --check '**/*.md'" + }, + "devDependencies": { + "markdownlint-cli": "^0.44.0", + "prettier": "^3.5.0" + } +} From 11faaecdd7dd616a16d39b1c92aa53cc96ff1b62 Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Thu, 23 Apr 2026 10:27:31 +0300 Subject: [PATCH 4/6] feat(install): add install script for Drupal AGENTS.md and knowledge base --- experimental/README.md | 125 +++++++++++++++++++++++ experimental/install.sh | 215 ++++++++++++++++++++++++++++++++++++++++ package.json | 4 +- 3 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 experimental/README.md create mode 100755 experimental/install.sh diff --git a/experimental/README.md b/experimental/README.md new file mode 100644 index 0000000..23d713a --- /dev/null +++ b/experimental/README.md @@ -0,0 +1,125 @@ +# 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 | +| `--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..1a9a623 --- /dev/null +++ b/experimental/install.sh @@ -0,0 +1,215 @@ +#!/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 +# --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 index 8845139..6210190 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "private": true, "description": "AI Agent Development Guides for Drupal", "scripts": { - "lint": "markdownlint '**/*.md'", - "lint:fix": "markdownlint --fix '**/*.md' && prettier --write '**/*.md'", "format": "prettier --write '**/*.md'", + "lint": "markdownlint '**/*.md'", + "lint:fix": "prettier --write '**/*.md' && markdownlint --fix '**/*.md'", "check": "markdownlint '**/*.md' && prettier --check '**/*.md'" }, "devDependencies": { From 129d9af1ee1ed4c7f754fd41e23a9c483a8cdcb0 Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Thu, 23 Apr 2026 10:31:25 +0300 Subject: [PATCH 5/6] docs(install): add branch option to install documentation --- experimental/README.md | 19 ++++++++++--------- experimental/install.sh | 1 + 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/experimental/README.md b/experimental/README.md index 23d713a..dbefb72 100644 --- a/experimental/README.md +++ b/experimental/README.md @@ -79,15 +79,16 @@ wget -qO- https://raw.githubusercontent.com/amazeeio/drupal-agents-md/main/exper ### Install Options -| Option | Description | -| ------------------- | ----------------------------------------- | -| `--variant=vanilla` | Vanilla variant (default) | -| `--variant=ddev` | DDEV variant | -| `--variant=lagoon` | Lagoon variant | -| `--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 | +| 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 diff --git a/experimental/install.sh b/experimental/install.sh index 1a9a623..4d72198 100755 --- a/experimental/install.sh +++ b/experimental/install.sh @@ -12,6 +12,7 @@ # --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 From 099ac31d88b14d548c108c64efb4bf3476a95fd5 Mon Sep 17 00:00:00 2001 From: Dimitris Spachos Date: Thu, 23 Apr 2026 10:37:55 +0300 Subject: [PATCH 6/6] docs: Update README with information on experimental architecture feature --- .gitignore | 1 + README.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 1dbae2a..7acc910 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ .idea/ node_modules/ package-lock.json +.pi diff --git a/README.md b/README.md index 61c72c7..019d9b8 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ 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. +> **🧪 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