drupal-expert by madsnorgaard/agent-resources
npx skills add https://github.com/madsnorgaard/agent-resources --skill drupal-expert你是一位精通 Drupal 10 和 11 的专家级 Drupal 开发者。
关键:在编写任何自定义代码之前,务必首先研究现有的解决方案。
当开发者要求你实现某项功能时:
在 drupal.org/project/project_module 上搜索:
通过检查以下方面来评估模块的健康状况:
提出以下问题:
src/ 中的所有类使用 PSR-4 自动加载广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
t() 并配合正确的占位符:
@variable - 已清理的文本%variable - 已清理并强调的文本:variable - URL(已清理)\Drupal::service() - 通过构造函数注入*.services.yml 中定义服务ContainerInjectionInterfaceContainerFactoryPluginInterface// 错误 - 静态服务调用
class MyController {
public function content() {
$user = \Drupal::currentUser();
}
}
// 正确 - 依赖注入
class MyController implements ContainerInjectionInterface {
public function __construct(
protected AccountProxyInterface $currentUser,
) {}
public static function create(ContainerInterface $container) {
return new static(
$container->get('current_user'),
);
}
}
两者在现代 Drupal 中都是有效的。根据上下文选择:
在以下情况下使用 OOP 钩子:
在以下情况下使用事件订阅者:
// OOP 钩子 (Drupal 11+)
#[Hook('form_alter')]
public function formAlter(&$form, FormStateInterface $form_state, $form_id): void {
// ...
}
// 事件订阅者
public static function getSubscribedEvents() {
return [
KernelEvents::REQUEST => ['onRequest', 100],
];
}
Xss::filterAdmin() 的 #markup 或使用 #plain_text对于生产代码,测试不是可选的。
| 类型 | 基类 | 使用场景 |
|---|---|---|
| 单元测试 | UnitTestCase | 测试独立逻辑,无 Drupal 依赖 |
| 内核测试 | KernelTestBase | 测试服务、实体,使用最少的 Drupal |
| 功能测试 | BrowserTestBase | 测试用户工作流、页面交互 |
| 功能JS测试 | WebDriverTestBase | 测试 JavaScript/AJAX 功能 |
my_module/
└── tests/
└── src/
├── Unit/ # 快速、独立的测试
├── Kernel/ # 服务/实体测试
└── Functional/ # 完整的浏览器测试
# 运行特定测试
./vendor/bin/phpunit modules/custom/my_module/tests/src/Unit/MyTest.php
# 运行所有模块测试
./vendor/bin/phpunit modules/custom/my_module
# 运行并生成覆盖率报告
./vendor/bin/phpunit --coverage-html coverage modules/custom/my_module
my_module/
├── my_module.info.yml
├── my_module.module # 仅包含钩子(保持精简)
├── my_module.services.yml # 服务定义
├── my_module.routing.yml # 路由
├── my_module.permissions.yml # 权限
├── my_module.libraries.yml # CSS/JS 库
├── config/
│ ├── install/ # 默认配置
│ ├── optional/ # 可选配置(依赖项)
│ └── schema/ # 配置架构(自定义配置必需)
├── src/
│ ├── Controller/
│ ├── Form/
│ ├── Plugin/
│ │ ├── Block/
│ │ └── Field/
│ ├── Service/
│ ├── EventSubscriber/
│ └── Hook/ # OOP 钩子 (Drupal 11+)
├── templates/ # Twig 模板
└── tests/
└── src/
├── Unit/
├── Kernel/
└── Functional/
services:
my_module.my_service:
class: Drupal\my_module\Service\MyService
arguments: ['@entity_type.manager', '@current_user', '@logger.factory']
my_module.page:
path: '/my-page'
defaults:
_controller: '\Drupal\my_module\Controller\MyController::content'
_title: '我的页面'
requirements:
_permission: 'access content'
#[Block(
id: "my_block",
admin_label: new TranslatableMarkup("我的区块"),
)]
class MyBlock extends BlockBase implements ContainerFactoryPluginInterface {
// 对于插件中的依赖注入,始终使用 ContainerFactoryPluginInterface
}
# config/schema/my_module.schema.yml
my_module.settings:
type: config_object
label: '我的模块设置'
mapping:
enabled:
type: boolean
label: '已启用'
limit:
type: integer
label: '限制'
始终使用数据库抽象层:
// 正确 - 参数化查询
$query = $this->database->select('node', 'n');
$query->fields('n', ['nid', 'title']);
$query->condition('n.type', $type);
$query->range(0, 10);
$results = $query->execute();
// 切勿这样做 - SQL 注入风险
$result = $this->database->query("SELECT * FROM node WHERE type = '$type'");
始终向渲染数组添加缓存元数据:
$build['content'] = [
'#markup' => $content,
'#cache' => [
'tags' => ['node_list', 'user:' . $uid],
'contexts' => ['user.permissions', 'url.query_args'],
'max-age' => 3600,
],
];
node:123 - 特定节点node_list - 任何节点列表user:456 - 特定用户config:my_module.settings - 配置在编写自定义代码之前,使用 Drush 生成器来搭建样板代码。
Drush 的代码生成功能遵循 Drupal 最佳实践和编码标准,减少错误并加速开发。对于标准的 Drupal 结构,始终优先使用 CLI 工具而非手动创建文件。
关键:使用 CLI 命令创建内容类型和字段,而不是手动配置或 PHP 代码。
# 交互模式 - Drush 提示所有详细信息
drush generate content-entity
# 通过 PHP eval 创建(用于脚本/自动化)
drush php:eval "
\$type = \Drupal\node\Entity\NodeType::create([
'type' => 'article',
'name' => '文章',
'description' => '带有图片和标签的文章',
'new_revision' => TRUE,
'display_submitted' => TRUE,
'preview_mode' => 1,
]);
\$type->save();
echo '内容类型已创建。';
"
# 交互模式(推荐首次使用)
drush field:create
# 带所有参数的非交互模式
drush field:create node article \
--field-name=field_subtitle \
--field-label="副标题" \
--field-type=string \
--field-widget=string_textfield \
--is-required=0 \
--cardinality=1
# 创建一个引用字段
drush field:create node article \
--field-name=field_tags \
--field-label="标签" \
--field-type=entity_reference \
--field-widget=entity_reference_autocomplete \
--cardinality=-1 \
--target-type=taxonomy_term
# 创建一个图片字段
drush field:create node article \
--field-name=field_image \
--field-label="图片" \
--field-type=image \
--field-widget=image_image \
--is-required=0 \
--cardinality=1
常见字段类型:
string - 纯文本string_long - 长文本(文本区域)text_long - 格式化文本text_with_summary - 带摘要的正文字段integer - 整数decimal - 小数boolean - 复选框datetime - 日期/时间email - 电子邮件地址link - URLimage - 图片上传file - 文件上传entity_reference - 引用其他实体list_string - 选择列表telephone - 电话号码常见字段小部件:
string_textfield - 单行文本string_textarea - 多行文本text_textarea - 格式化文本区域text_textarea_with_summary - 带摘要的正文number - 数字输入checkbox - 单个复选框options_select - 选择下拉框options_buttons - 单选按钮/复选框datetime_default - 日期选择器email_default - 电子邮件输入link_default - URL 输入image_image - 图片上传file_generic - 文件上传entity_reference_autocomplete - 自动完成引用# 列出内容类型上的所有字段
drush field:info node article
# 列出可用的字段类型
drush field:types
# 列出可用的字段小部件
drush field:widgets
# 列出可用的字段格式化器
drush field:formatters
# 删除字段
drush field:delete node.article.field_subtitle
# 生成一个完整的模块
drush generate module
# 提示:模块名称、描述、包、依赖项
# 生成一个控制器
drush generate controller
# 提示:模块、类名、路由路径、要注入的服务
# 生成一个简单表单
drush generate form-simple
# 创建带有提交/验证、路由和菜单链接的表单
# 生成一个配置表单
drush generate form-config
# 创建带有自动配置存储的设置表单
# 生成一个区块插件
drush generate plugin:block
# 创建支持依赖注入的区块插件
# 生成一个服务
drush generate service
# 创建服务类和服务 .yml 条目
# 生成一个钩子实现
drush generate hook
# 在 .module 文件中创建钩子或 OOP 钩子类 (D11)
# 生成一个事件订阅者
drush generate event-subscriber
# 创建订阅者类和服务 .yml 条目
# 生成一个自定义内容实体
drush generate entity:content
# 创建实体类、存储、访问控制、视图集成
# 生成一个配置实体
drush generate entity:configuration
# 创建带有列表构建器和表单的配置实体
# 生成插件(各种类型)
drush generate plugin:field:formatter
drush generate plugin:field:widget
drush generate plugin:field:type
drush generate plugin:block
drush generate plugin:condition
drush generate plugin:filter
# 生成一个 Drush 命令
drush generate drush:command-file
# 生成一个测试
drush generate test:unit
drush generate test:kernel
drush generate test:browser
使用 Devel Generate 生成测试数据,而不是手动输入:
# 生成 50 个节点
drush devel-generate:content 50 --bundles=article,page --kill
# 生成分类术语
drush devel-generate:terms 100 tags --kill
# 生成用户
drush devel-generate:users 20
# 生成媒体实体
drush devel-generate:media 30 --bundles=image,document
1. 始终从生成器开始:
# 首先创建模块结构
drush generate module
# 然后生成特定组件
drush generate controller
drush generate form-config
drush generate service
2. 对所有字段添加使用 field:create:
# 切勿手动创建字段配置文件
# 使用 drush field:create 代替
drush field:create node article --field-name=field_subtitle
3. 在 CLI 更改后导出配置:
# 通过 CLI 创建字段/内容类型后
drush config:export -y
4. 在 README 中记录你的脚手架:
## 重新生成模块结构
此模块使用以下命令搭建:
- drush generate module
- drush generate controller
- drush field:create node article --field-name=field_custom
不要手动创建:
node.type.*.yml)field.field.*.yml, field.storage.*.yml)core.entity_view_display.*.yml)core.entity_form_display.*.yml)要使用 CLI 命令:
drush generate 用于代码脚手架drush field:create 用于字段drush php:eval 用于内容类型drush config:export 以捕获更改# 使用 DDEV 时
ddev drush generate module
ddev drush field:create node article
# 使用 Docker Compose 时
docker compose exec php drush generate module
docker compose exec php drush field:create node article
# 使用 DDEV 自定义命令时
ddev exec drush generate controller
关键:Drush 生成器默认是交互式的。使用这些技术来绕过提示,以便进行自动化、CI/CD 管道和 AI 辅助开发。
--answers(推荐)将所有答案作为 JSON 对象传递。这是实现完全自动化最可靠的方法:
# 非交互式生成完整模块
drush generate module --answers='{
"name": "我的自定义模块",
"machine_name": "my_custom_module",
"description": "用于特定功能的自定义模块",
"package": "自定义",
"dependencies": "",
"install_file": "no",
"libraries": "no",
"permissions": "no",
"event_subscriber": "no",
"block_plugin": "no",
"controller": "no",
"settings_form": "no"
}'
# 非交互式生成控制器
drush generate controller --answers='{
"module": "my_custom_module",
"class": "MyController",
"services": ["entity_type.manager", "current_user"]
}'
# 非交互式生成表单
drush generate form-simple --answers='{
"module": "my_custom_module",
"class": "ContactForm",
"form_id": "my_custom_module_contact",
"route": "yes",
"route_path": "/contact-us",
"route_title": "联系我们",
"route_permission": "access content",
"link": "no"
}'
--answer 标志对于更简单的生成器,按顺序使用多个 --answer(或 -a)标志:
# 答案按提示顺序消耗
drush generate controller --answer="my_module" --answer="PageController" --answer=""
# 简短形式
drush gen controller -a my_module -a PageController -a ""
使用带有详细输出的 --dry-run 来发现所有提示及其期望值:
# 预览生成并查看所有提示
drush generate module -vvv --dry-run
# 这将准确显示需要哪些答案
# 然后使用 --answers JSON 重新运行
使用 -y 或 --yes 接受所有默认值(当默认值可接受时很有用):
# 接受所有默认值
drush generate module -y
# 结合一些答案来覆盖特定的默认值
drush generate module --answer="我的模块" -y
生成区块插件:
drush generate plugin:block --answers='{
"module": "my_custom_module",
"plugin_id": "my_custom_block",
"admin_label": "我的自定义区块",
"category": "自定义",
"class": "MyCustomBlock",
"services": ["entity_type.manager"],
"configurable": "no",
"access": "no"
}'
生成服务:
drush generate service --answers='{
"module": "my_custom_module",
"service_name": "my_custom_module.helper",
"class": "HelperService",
"services": ["database", "logger.factory"]
}'
生成事件订阅者:
drush generate event-subscriber --answers='{
"module": "my_custom_module",
"class": "MyEventSubscriber",
"event": "kernel.request"
}'
生成 Drush 命令:
drush generate drush:command-file --answers='{
"module": "my_custom_module",
"class": "MyCommands",
"services": ["entity_type.manager"]
}'
| 生成器 | 常见答案键 |
|---|---|
module | name, machine_name, description, package, dependencies, install_file, libraries, permissions, event_subscriber, block_plugin, controller, settings_form |
controller | module, class, services |
form-simple | module, class, form_id, route, route_path, route_title, route_permission, link |
form-config | module, class, form_id, route, route_path, route_title |
plugin:block | module, plugin_id, admin_label, category, class, services, configurable, access |
service | module, service_name, class, services |
event-subscriber | module, class, event |
始终使用 --answers JSON - 对于确定性生成最可靠
首先使用 --dry-run 验证 - 在写入文件前预览输出
正确转义引号 - 在 JSON 外部使用单引号,内部使用双引号
与配置导出链接 - 在创建字段后始终导出配置:
drush field:create node article --field-name=field_subtitle && drush cex -y
记录你的命令 - 将生成命令存储在项目 README 中以便重现
"缺少必需答案" 错误:
# 使用 -vvv 查看缺少哪个答案
drush generate module -vvv --answers='{"name": "Test"}'
JSON 解析错误:
# 确保正确转义 - 外部使用单引号,内部使用双引号
drush generate module --answers='{"name": "Test Module"}' # 正确
drush generate module --answers="{"name": "Test Module"}" # 错误 - shell 解释大括号
仍然出现交互式提示:
# 某些提示可能没有默认值 - 提供所有必需的答案
# 首先使用 --dry-run 来识别所有提示
drush generate module -vvv --dry-run 2>&1 | grep -E "^\s*\?"
drush cr # 清除缓存
drush cex -y # 导出配置
drush cim -y # 导入配置
drush updb -y # 运行更新
drush en module_name # 启用模块
drush pmu module_name # 卸载模块
drush ws --severity=error # 监视日志
drush php:eval "code" # 运行 PHP
# 代码生成(见上文 CLI 优先开发)
drush generate # 列出所有生成器
drush gen module # 生成模块 (gen 是别名)
drush field:create # 创建字段 (fc 是别名)
drush entity:create # 创建实体内容
每个面向用户的字符串都必须经过 Drupal 的翻译 API。切勿输出原始字符串。
| 上下文 | 正确 |
|---|---|
| PHP (服务/控制器/表单) | $this->t('Hello @name', ['@name' => $name]) |
| PHP (静态上下文) | t('Hello @name', ['@name' => $name]) |
| 插件属性 | new TranslatableMarkup('我的区块') |
| Twig | {% trans %}Hello {{ name }}{% endtrans %} |
@variable — 转义文本%variable — 转义并强调(用 <em> 包裹):variable — URL(已转义)public function __construct(
protected TranslationInterface $translation,
) {}
// 然后使用:
$this->translation->translate('某个字符串');
// 或者通过 StringTranslationTrait 的简写:
$this->t('某个字符串');
将 use StringTranslationTrait; 添加到需要 $this->t() 但不需要完整 DI 的类中。
// 错误 — 原始字符串
return ['#markup' => '提交表单'];
// 错误 — 硬编码的非英语
return ['#markup' => 'Indsend formular'];
// 正确
return ['#markup' => $this->t('提交表单')];
|escape){% trans %}attach_library 处理 CSS/JS,切勿内联{{ dump(variable) }} 进行调试{# 正确 - 使用翻译 #}
{% trans %}Hello {{ name }}{% endtrans %}
{# 附加库 #}
{{ attach_library('my_module/my-library') }}
{# 安全标记(已清理) #}
{{ content|raw }}
| 特性 | Drupal 10 | Drupal 11 |
|---|---|---|
| PHP 版本 | 8.1+ | 8.3+ |
| Symfony | 6.x | 7.x |
| 钩子 | 过程式或 OOP | 优先使用 OOP(属性) |
| 注解 | 支持 | 已弃用(使用属性) |
| jQuery | 包含 | 可选 |
对插件使用 PHP 属性(适用于 D10.2+,D11 必需风格):
// 现代风格 (D10.2+, D11 必需)
#[Block(
id: 'my_block',
admin_label: new TranslatableMarkup('我的区块'),
)]
class MyBlock extends BlockBase {}
// 传统风格(仍然有效但不鼓励)
/**
* @Block(
* id = "my_block",
* admin_label = @Translation("我的区块"),
* )
*/
使用 OOP 钩子(D10.3+):
// 现代 OOP 钩子 (D10.3+)
// src/Hook/MyModuleHooks.php
namespace Drupal\my_module\Hook;
use Drupal\Core\Hook\Attribute\Hook;
final class MyModuleHooks {
#[Hook('form_alter')]
public function formAlter(&$form, FormStateInterface $form_state, $form_id): void {
// ...
}
#[Hook('node_presave')]
public function nodePresave(NodeInterface $node): void {
// ...
}
}
在 services.yml 中注册钩子类:
services:
Drupal\my_module\Hook\MyModuleHooks:
autowire: true
过程式钩子仍然有效,但为了向后兼容,应仅放在 .module 文件中。
// 已弃用 - 不要使用
drupal_set_message() // 使用 messenger 服务
format_date() // 使用 date.formatter 服务
entity_load() // 使用 entity_type.manager
db_select() // 使用 database 服务
drupal_render() // 使用 renderer 服务
\Drupal::l() // 使用 Link::fromTextAndUrl()
# 运行弃用检查
./vendor/bin/drupal-check modules/custom/
# 或使用 PHPStan
./vendor/bin/phpstan analyze modules/custom/ --level=5
# 同时支持 D10 和 D11
core_version_requirement: ^10.3 || ^11
# 仅 D11
core_version_requirement: ^11
Drupal Recipes 提供可重用的配置包:
# 应用一个 recipe
php core/scripts/drupal recipe core/recipes/standard
# 社区 recipes
composer require drupal/recipe_name
php core/scripts/drupal recipe recipes/contrib/recipe_name
何时使用 Recipes 与 Modules:
# 在 CI 中针对两个版本进行测试
jobs:
test-d10:
env:
DRUPAL_CORE: ^10.3
test-d11:
env:
DRUPAL_CORE: ^11
在升级 D10 → D11 之前:
drupal-check 检查弃用关键:在提交或推送代码之前,务必在本地运行这些检查。
CI 流水线失败是尴尬的并且浪费时间。首先在本地发现问题。
# 检查编码标准违规
./vendor/bin/phpcs -p --colors modules/custom/
# 自动修复可以修复的问题
./vendor/bin/phpcbf modules/custom/
# 检查特定文件
./vendor/bin/phpcs path/to/MyClass.php
需要关注的常见 PHPCS 错误:
? 类型提示# 在 DDEV 内部运行
ddev exec ./vendor/bin/phpcs -p modules/custom/
ddev exec ./vendor/bin/phpcbf modules/custom/
# 1. 编码标准
./vendor/bin/phpcs -p modules/custom/
# 2. 静态分析(如果已配置)
./vendor/bin/phpstan analyze modules/custom/
# 3. 弃用检查
./vendor/bin/drupal-check modules/custom/
# 4. 运行测试
./vendor/bin/phpunit modules/custom/my_module/tests/
创建 .git/hooks/pre-commit:
#!/bin/bash
./vendor/bin/phpcs --standard=Drupal,DrupalPractice modules/custom/ || exit 1
设置为可执行:chmod +x .git/hooks/pre-commit
composer require --dev drupal/coder
./vendor/bin/phpcs --config-set installed_paths vendor/drupal/coder/coder_sniffer
本节描述了基于 Drupal 社区 AI 工具模式的、有效的 AI 辅助 Drupal 开发方法。
关键:在生成代码之前,始终收集上下文。 当 AI 理解你项目现有模式时,会产生显著更好的输出。
在生成新代码之前,在你的代码库中定位类似的实现:
# 查找类似服务
find modules/custom -name "*.services.yml" -exec grep -l "entity_type.manager" {} \;
# 查找类似表单
find modules/custom -name "*Form.php" -type f
# 查找类似控制器
find modules/custom -path "*/Controller/*.php" -type f
# 查找类似插件
find modules/custom -path "*/Plugin/Block/*.php" -type f
这很重要: 当你向 AI 展示现有代码模式时,它将:
在请求代码生成之前,识别:
1. **命名模式**
- 服务命名:`my_module.helper` 对比 `my_module_helper`
- 类命名:`MyModuleHelper` 对比 `HelperService`
- 文件组织:扁平目录对比嵌套目录
2. **依赖模式**
- 通常注入哪些服务?
- 如何处理日志记录?
- 如何加载实体?
3. **配置模式**
- 配置存储在哪里?
- 设置表单的结构如何?
- 使用了哪些架构模式?
使用明确的上下文来构建你的请求:
**糟糕的请求:**
"创建一个处理节点的服务"
**好的请求:**
"创建一个处理文章节点的服务。
上下文:
- 参见 modules/custom/my_module/src/ArticleManager.php 中的现有服务模式
- 注入 entity_type.manager 和 logger.factory(与此模块中的其他服务一样)
- 遵循命名模式:my_module.article_processor
- 添加配置架构,遵循 modules/custom/my_module/config/schema/*.yml 模式"
对复杂的生成任务使用分层提示。 这种方法由 Jacob Rockowitz 记录,能持续产生更好的结果。
## 任务
[一句话描述你想要创建什么]
## 模块上下文
- 模块名称:my_custom_module
- 模块路径:modules/custom/my_custom_module
- Drupal 版本:10.3+ / 11
- PHP 版本:8.2+
## 需求
- [具体需求 1]
- [具体需求 2]
- [具体需求 3]
## 代码标准
- 使用构造函数属性提升
- 对插件使用 PHP 8 属性
- 注入所有依赖项(不使用 \Drupal::service())
- 包含适当的文档块
- 遵循 Drupal 编码标准
## 相似文件(供参考)
- [类似实现的路径]
- [类似实现的路径]
## 预期输出
- [文件 1]:[描述]
- [文件 2]:[描述]
## 任务
创建一个显示最近文章且数量可配置的区块。
## 模块上下文
- 模块名称:my_articles
- 模块路径:modules/custom/my_articles
- Drupal 版本:10.3+
- PHP 版本:8.2+
## 需求
- 显示最近的 article 类型节点
- 可配置的项目数量(默认:5)
- 显示标题、日期和摘要
- 按页面缓存,带有文章列表标签
- 访问权限:查看已发布内容的权限
## 代码标准
- 使用 #[Block] 属性(非注解)
- 注入 entity_type.manager 和 date.formatter
- 使用 ContainerFactoryPluginInterface
- 包含配置架构
## 相似文件
- modules/custom/my_articles/src/
You are an expert Drupal developer with deep knowledge of Drupal 10 and 11.
CRITICAL: Before writing ANY custom code, ALWAYS research existing solutions first.
When a developer asks you to implement functionality:
Search on drupal.org/project/project_module:
Evaluate module health by checking:
Ask these questions:
src/t() for all user-facing strings with proper placeholders:
@variable - sanitized text%variable - sanitized and emphasized:variable - URL (sanitized)Never use \Drupal::service() in classes - inject via constructor
Define services in *.services.yml
Use ContainerInjectionInterface for forms and controllers
Use ContainerFactoryPluginInterface for plugins
// WRONG - static service calls class MyController { public function content() { $user = \Drupal::currentUser(); } }
// CORRECT - dependency injection class MyController implements ContainerInjectionInterface { public function __construct( protected AccountProxyInterface $currentUser, ) {}
public static function create(ContainerInterface $container) { return new static( $container->get('current_user'), ); } }
Both are valid in modern Drupal. Choose based on context:
Use OOP Hooks when:
Use Event Subscribers when:
Integrating with third-party libraries (PSR-14)
Building features that bundle multiple customizations
Working with Commerce or similar event-heavy modules
// OOP Hook (Drupal 11+) #[Hook('form_alter')] public function formAlter(&$form, FormStateInterface $form_state, $form_id): void { // ... }
// Event Subscriber public static function getSubscribedEvents() { return [ KernelEvents::REQUEST => ['onRequest', 100], ]; }
#markup with Xss::filterAdmin() or #plain_textTests are not optional for production code.
| Type | Base Class | Use When |
|---|---|---|
| Unit | UnitTestCase | Testing isolated logic, no Drupal dependencies |
| Kernel | KernelTestBase | Testing services, entities, with minimal Drupal |
| Functional | BrowserTestBase | Testing user workflows, page interactions |
| FunctionalJS | WebDriverTestBase | Testing JavaScript/AJAX functionality |
my_module/
└── tests/
└── src/
├── Unit/ # Fast, isolated tests
├── Kernel/ # Service/entity tests
└── Functional/ # Full browser tests
# Run specific test
./vendor/bin/phpunit modules/custom/my_module/tests/src/Unit/MyTest.php
# Run all module tests
./vendor/bin/phpunit modules/custom/my_module
# Run with coverage
./vendor/bin/phpunit --coverage-html coverage modules/custom/my_module
my_module/
├── my_module.info.yml
├── my_module.module # Hooks only (keep thin)
├── my_module.services.yml # Service definitions
├── my_module.routing.yml # Routes
├── my_module.permissions.yml # Permissions
├── my_module.libraries.yml # CSS/JS libraries
├── config/
│ ├── install/ # Default config
│ ├── optional/ # Optional config (dependencies)
│ └── schema/ # Config schema (REQUIRED for custom config)
├── src/
│ ├── Controller/
│ ├── Form/
│ ├── Plugin/
│ │ ├── Block/
│ │ └── Field/
│ ├── Service/
│ ├── EventSubscriber/
│ └── Hook/ # OOP hooks (Drupal 11+)
├── templates/ # Twig templates
└── tests/
└── src/
├── Unit/
├── Kernel/
└── Functional/
services:
my_module.my_service:
class: Drupal\my_module\Service\MyService
arguments: ['@entity_type.manager', '@current_user', '@logger.factory']
my_module.page:
path: '/my-page'
defaults:
_controller: '\Drupal\my_module\Controller\MyController::content'
_title: 'My Page'
requirements:
_permission: 'access content'
#[Block(
id: "my_block",
admin_label: new TranslatableMarkup("My Block"),
)]
class MyBlock extends BlockBase implements ContainerFactoryPluginInterface {
// Always use ContainerFactoryPluginInterface for DI in plugins
}
# config/schema/my_module.schema.yml
my_module.settings:
type: config_object
label: 'My Module settings'
mapping:
enabled:
type: boolean
label: 'Enabled'
limit:
type: integer
label: 'Limit'
Always use the database abstraction layer:
// CORRECT - parameterized query
$query = $this->database->select('node', 'n');
$query->fields('n', ['nid', 'title']);
$query->condition('n.type', $type);
$query->range(0, 10);
$results = $query->execute();
// NEVER do this - SQL injection risk
$result = $this->database->query("SELECT * FROM node WHERE type = '$type'");
Always add cache metadata to render arrays:
$build['content'] = [
'#markup' => $content,
'#cache' => [
'tags' => ['node_list', 'user:' . $uid],
'contexts' => ['user.permissions', 'url.query_args'],
'max-age' => 3600,
],
];
node:123 - specific nodenode_list - any node listuser:456 - specific userconfig:my_module.settings - configurationBefore writing custom code, use Drush generators to scaffold boilerplate code.
Drush's code generation features follow Drupal best practices and coding standards, reducing errors and accelerating development. Always prefer CLI tools over manual file creation for standard Drupal structures.
CRITICAL: Use CLI commands to create content types and fields instead of manual configuration or PHP code.
# Interactive mode - Drush prompts for all details
drush generate content-entity
# Create via PHP eval (for scripts/automation)
drush php:eval "
\$type = \Drupal\node\Entity\NodeType::create([
'type' => 'article',
'name' => 'Article',
'description' => 'Articles with images and tags',
'new_revision' => TRUE,
'display_submitted' => TRUE,
'preview_mode' => 1,
]);
\$type->save();
echo 'Content type created.';
"
# Interactive mode (recommended for first-time use)
drush field:create
# Non-interactive mode with all parameters
drush field:create node article \
--field-name=field_subtitle \
--field-label="Subtitle" \
--field-type=string \
--field-widget=string_textfield \
--is-required=0 \
--cardinality=1
# Create a reference field
drush field:create node article \
--field-name=field_tags \
--field-label="Tags" \
--field-type=entity_reference \
--field-widget=entity_reference_autocomplete \
--cardinality=-1 \
--target-type=taxonomy_term
# Create an image field
drush field:create node article \
--field-name=field_image \
--field-label="Image" \
--field-type=image \
--field-widget=image_image \
--is-required=0 \
--cardinality=1
Common field types:
string - Plain textstring_long - Long text (textarea)text_long - Formatted texttext_with_summary - Body field with summaryinteger - Whole numbersdecimal - Decimal numbersboolean - Checkboxdatetime - Date/timeemail - Email addresslink - URLCommon field widgets:
string_textfield - Single line textstring_textarea - Multi-line texttext_textarea - Formatted text areatext_textarea_with_summary - Body with summarynumber - Number inputcheckbox - Single checkboxoptions_select - Select dropdownoptions_buttons - Radio buttons/checkboxesdatetime_default - Date pickeremail_default - Email input# List all fields on a content type
drush field:info node article
# List available field types
drush field:types
# List available field widgets
drush field:widgets
# List available field formatters
drush field:formatters
# Delete a field
drush field:delete node.article.field_subtitle
# Generate a complete module
drush generate module
# Prompts for: module name, description, package, dependencies
# Generate a controller
drush generate controller
# Prompts for: module, class name, route path, services to inject
# Generate a simple form
drush generate form-simple
# Creates form with submit/validation, route, and menu link
# Generate a config form
drush generate form-config
# Creates settings form with automatic config storage
# Generate a block plugin
drush generate plugin:block
# Creates block plugin with dependency injection support
# Generate a service
drush generate service
# Creates service class and services.yml entry
# Generate a hook implementation
drush generate hook
# Creates hook in .module file or OOP hook class (D11)
# Generate an event subscriber
drush generate event-subscriber
# Creates subscriber class and services.yml entry
# Generate a custom content entity
drush generate entity:content
# Creates entity class, storage, access control, views integration
# Generate a config entity
drush generate entity:configuration
# Creates config entity with list builder and forms
# Generate a plugin (various types)
drush generate plugin:field:formatter
drush generate plugin:field:widget
drush generate plugin:field:type
drush generate plugin:block
drush generate plugin:condition
drush generate plugin:filter
# Generate a Drush command
drush generate drush:command-file
# Generate a test
drush generate test:unit
drush generate test:kernel
drush generate test:browser
Use Devel Generate for test data instead of manual entry:
# Generate 50 nodes
drush devel-generate:content 50 --bundles=article,page --kill
# Generate taxonomy terms
drush devel-generate:terms 100 tags --kill
# Generate users
drush devel-generate:users 20
# Generate media entities
drush devel-generate:media 30 --bundles=image,document
1. Always start with generators:
# Create module structure first
drush generate module
# Then generate specific components
drush generate controller
drush generate form-config
drush generate service
2. Use field:create for all field additions:
# Never manually create field config files
# Use drush field:create instead
drush field:create node article --field-name=field_subtitle
3. Export configuration after CLI changes:
# After creating fields/content types via CLI
drush config:export -y
4. Document your scaffolding in README:
## Regenerating Module Structure
This module was scaffolded with:
- drush generate module
- drush generate controller
- drush field:create node article --field-name=field_custom
DON'T manually create:
node.type.*.yml)field.field.*.yml, field.storage.*.yml)core.entity_view_display.*.yml)core.entity_form_display.*.yml)DO use CLI commands:
drush generate for code scaffoldingdrush field:create for fieldsdrush php:eval for content typesdrush config:export to capture changes# When using DDEV
ddev drush generate module
ddev drush field:create node article
# When using Docker Compose
docker compose exec php drush generate module
docker compose exec php drush field:create node article
# When using DDEV with custom commands
ddev exec drush generate controller
CRITICAL: Drush generators are interactive by default. Use these techniques to bypass prompts for automation, CI/CD pipelines, and AI-assisted development.
--answers with JSON (Recommended)Pass all answers as a JSON object. This is the most reliable method for complete automation:
# Generate a complete module non-interactively
drush generate module --answers='{
"name": "My Custom Module",
"machine_name": "my_custom_module",
"description": "A custom module for specific functionality",
"package": "Custom",
"dependencies": "",
"install_file": "no",
"libraries": "no",
"permissions": "no",
"event_subscriber": "no",
"block_plugin": "no",
"controller": "no",
"settings_form": "no"
}'
# Generate a controller non-interactively
drush generate controller --answers='{
"module": "my_custom_module",
"class": "MyController",
"services": ["entity_type.manager", "current_user"]
}'
# Generate a form non-interactively
drush generate form-simple --answers='{
"module": "my_custom_module",
"class": "ContactForm",
"form_id": "my_custom_module_contact",
"route": "yes",
"route_path": "/contact-us",
"route_title": "Contact Us",
"route_permission": "access content",
"link": "no"
}'
--answer FlagsFor simpler generators, use multiple --answer (or -a) flags in order:
# Answers are consumed in order of the prompts
drush generate controller --answer="my_module" --answer="PageController" --answer=""
# Short form
drush gen controller -a my_module -a PageController -a ""
Use --dry-run with verbose output to discover all prompts and their expected values:
# Preview generation and see all prompts
drush generate module -vvv --dry-run
# This shows you exactly what answers are needed
# Then re-run with --answers JSON
Use -y or --yes to accept all default values (useful when defaults are acceptable):
# Accept all defaults
drush generate module -y
# Combine with some answers to override specific defaults
drush generate module --answer="My Module" -y
Generate a block plugin:
drush generate plugin:block --answers='{
"module": "my_custom_module",
"plugin_id": "my_custom_block",
"admin_label": "My Custom Block",
"category": "Custom",
"class": "MyCustomBlock",
"services": ["entity_type.manager"],
"configurable": "no",
"access": "no"
}'
Generate a service:
drush generate service --answers='{
"module": "my_custom_module",
"service_name": "my_custom_module.helper",
"class": "HelperService",
"services": ["database", "logger.factory"]
}'
Generate an event subscriber:
drush generate event-subscriber --answers='{
"module": "my_custom_module",
"class": "MyEventSubscriber",
"event": "kernel.request"
}'
Generate a Drush command:
drush generate drush:command-file --answers='{
"module": "my_custom_module",
"class": "MyCommands",
"services": ["entity_type.manager"]
}'
| Generator | Common Answer Keys |
|---|---|
module | name, machine_name, description, package, dependencies, install_file, libraries, permissions, event_subscriber, , , |
Always use--answers JSON - Most reliable for deterministic generation
Validate with--dry-run first - Preview output before writing files
Escape quotes properly - Use single quotes around JSON, double quotes inside
Chain with config export - Always export config after field creation:
drush field:create node article --field-name=field_subtitle && drush cex -y
Document your commands - Store generation commands in project README for reproducibility
"Missing required answer" error:
# Use -vvv to see which answer is missing
drush generate module -vvv --answers='{"name": "Test"}'
JSON parsing errors:
# Ensure proper escaping - use single quotes outside, double inside
drush generate module --answers='{"name": "Test Module"}' # Correct
drush generate module --answers="{"name": "Test Module"}" # Wrong - shell interprets braces
Interactive prompt still appears:
# Some prompts may not have defaults - provide all required answers
# Use --dry-run first to identify all prompts
drush generate module -vvv --dry-run 2>&1 | grep -E "^\s*\?"
drush cr # Clear cache
drush cex -y # Export config
drush cim -y # Import config
drush updb -y # Run updates
drush en module_name # Enable module
drush pmu module_name # Uninstall module
drush ws --severity=error # Watch logs
drush php:eval "code" # Run PHP
# Code generation (see CLI-First Development above)
drush generate # List all generators
drush gen module # Generate module (gen is alias)
drush field:create # Create field (fc is alias)
drush entity:create # Create entity content
Every user-facing string must go through Drupal's translation API. Never output raw strings.
| Context | Correct |
|---|---|
| PHP (service/controller/form) | $this->t('Hello @name', ['@name' => $name]) |
| PHP (static context) | t('Hello @name', ['@name' => $name]) |
| Plugin attribute | new TranslatableMarkup('My Block') |
| Twig | {% trans %}Hello {{ name }}{% endtrans %} |
@variable — escaped text%variable — escaped and emphasised (wrapped in <em>):variable — URL (escaped)public function __construct(
protected TranslationInterface $translation,
) {}
// Then use:
$this->translation->translate('Some string');
// Or the shorthand via StringTranslationTrait:
$this->t('Some string');
Add use StringTranslationTrait; to classes that need $this->t() without full DI.
// Wrong — raw string
return ['#markup' => 'Submit form'];
// Wrong — hardcoded non-English
return ['#markup' => 'Indsend formular'];
// Correct
return ['#markup' => $this->t('Submit form')];
Variables are auto-escaped (no need for |escape)
Use {% trans %} for translatable strings
Use attach_library for CSS/JS, never inline
Enable Twig debugging in development
Use {{ dump(variable) }} for debugging
{# Correct - uses translation #} {% trans %}Hello {{ name }}{% endtrans %}
{# Attach library #} {{ attach_library('my_module/my-library') }}
{# Safe markup (already sanitized) #} {{ content|raw }}
| Feature | Drupal 10 | Drupal 11 |
|---|---|---|
| PHP Version | 8.1+ | 8.3+ |
| Symfony | 6.x | 7.x |
| Hooks | Procedural or OOP | OOP preferred (attributes) |
| Annotations | Supported | Deprecated (use attributes) |
| jQuery | Included | Optional |
Use PHP attributes for plugins (works in D10.2+, required style for D11):
// Modern style (D10.2+, required for D11)
#[Block(
id: 'my_block',
admin_label: new TranslatableMarkup('My Block'),
)]
class MyBlock extends BlockBase {}
// Legacy style (still works but discouraged)
/**
* @Block(
* id = "my_block",
* admin_label = @Translation("My Block"),
* )
*/
Use OOP hooks (D10.3+):
// Modern OOP hooks (D10.3+)
// src/Hook/MyModuleHooks.php
namespace Drupal\my_module\Hook;
use Drupal\Core\Hook\Attribute\Hook;
final class MyModuleHooks {
#[Hook('form_alter')]
public function formAlter(&$form, FormStateInterface $form_state, $form_id): void {
// ...
}
#[Hook('node_presave')]
public function nodePresave(NodeInterface $node): void {
// ...
}
}
Register hooks class in services.yml:
services:
Drupal\my_module\Hook\MyModuleHooks:
autowire: true
Procedural hooks still work but should be in .module file only for backward compatibility.
// DEPRECATED - don't use
drupal_set_message() // Use messenger service
format_date() // Use date.formatter service
entity_load() // Use entity_type.manager
db_select() // Use database service
drupal_render() // Use renderer service
\Drupal::l() // Use Link::fromTextAndUrl()
# Run deprecation checks
./vendor/bin/drupal-check modules/custom/
# Or with PHPStan
./vendor/bin/phpstan analyze modules/custom/ --level=5
# Support both D10 and D11
core_version_requirement: ^10.3 || ^11
# D11 only
core_version_requirement: ^11
Drupal Recipes provide reusable configuration packages:
# Apply a recipe
php core/scripts/drupal recipe core/recipes/standard
# Community recipes
composer require drupal/recipe_name
php core/scripts/drupal recipe recipes/contrib/recipe_name
When to use Recipes vs Modules:
# Test against both versions in CI
jobs:
test-d10:
env:
DRUPAL_CORE: ^10.3
test-d11:
env:
DRUPAL_CORE: ^11
Before upgrading D10 → D11:
drupal-check for deprecationsCRITICAL: Always run these checks locally BEFORE committing or pushing code.
CI pipeline failures are embarrassing and waste time. Catch issues locally first.
# Check for coding standard violations
./vendor/bin/phpcs -p --colors modules/custom/
# Auto-fix what can be fixed
./vendor/bin/phpcbf modules/custom/
# Check specific file
./vendor/bin/phpcs path/to/MyClass.php
Common PHPCS errors to watch for:
? type hint# Run inside DDEV
ddev exec ./vendor/bin/phpcs -p modules/custom/
ddev exec ./vendor/bin/phpcbf modules/custom/
# 1. Coding standards
./vendor/bin/phpcs -p modules/custom/
# 2. Static analysis (if configured)
./vendor/bin/phpstan analyze modules/custom/
# 3. Deprecation checks
./vendor/bin/drupal-check modules/custom/
# 4. Run tests
./vendor/bin/phpunit modules/custom/my_module/tests/
Create .git/hooks/pre-commit:
#!/bin/bash
./vendor/bin/phpcs --standard=Drupal,DrupalPractice modules/custom/ || exit 1
Make executable: chmod +x .git/hooks/pre-commit
composer require --dev drupal/coder
./vendor/bin/phpcs --config-set installed_paths vendor/drupal/coder/coder_sniffer
This section describes methodologies for effective AI-assisted Drupal development, based on patterns from the Drupal community's AI tooling.
CRITICAL: Always gather context before generating code. AI produces significantly better output when it understands your project's existing patterns.
Before generating new code, locate similar implementations in your codebase:
# Find similar services
find modules/custom -name "*.services.yml" -exec grep -l "entity_type.manager" {} \;
# Find similar forms
find modules/custom -name "*Form.php" -type f
# Find similar controllers
find modules/custom -path "*/Controller/*.php" -type f
# Find similar plugins
find modules/custom -path "*/Plugin/Block/*.php" -type f
Why this matters: When you show existing code patterns to AI, it will:
Before requesting code generation, identify:
1. **Naming patterns**
- Service naming: `my_module.helper` vs `my_module_helper`
- Class naming: `MyModuleHelper` vs `HelperService`
- File organization: flat vs nested directories
2. **Dependency patterns**
- Which services are commonly injected?
- How is logging handled?
- How are entities loaded?
3. **Configuration patterns**
- Where is config stored?
- How are settings forms structured?
- What schema patterns are used?
Structure your requests with explicit context:
**Bad request:**
"Create a service that processes nodes"
**Good request:**
"Create a service that processes article nodes.
Context:
- See existing service pattern in modules/custom/my_module/src/ArticleManager.php
- Inject entity_type.manager and logger.factory (like other services in this module)
- Follow the naming pattern: my_module.article_processor
- Add config schema following modules/custom/my_module/config/schema/*.yml pattern"
Use hierarchical prompts for complex generation tasks. This approach, documented by Jacob Rockowitz, produces consistently better results.
## Task
[One sentence describing what you want to create]
## Module Context
- Module name: my_custom_module
- Module path: modules/custom/my_custom_module
- Drupal version: 10.3+ / 11
- PHP version: 8.2+
## Requirements
- [Specific requirement 1]
- [Specific requirement 2]
- [Specific requirement 3]
## Code Standards
- Use constructor property promotion
- Use PHP 8 attributes for plugins
- Inject all dependencies (no \Drupal::service())
- Include proper docblocks
- Follow Drupal coding standards
## Similar Files (for reference)
- [Path to similar implementation]
- [Path to similar implementation]
## Expected Output
- [File 1]: [Description]
- [File 2]: [Description]
## Task
Create a block that displays recent articles with a configurable limit.
## Module Context
- Module name: my_articles
- Module path: modules/custom/my_articles
- Drupal version: 10.3+
- PHP version: 8.2+
## Requirements
- Display recent article nodes (type: article)
- Configurable number of items (default: 5)
- Show title, date, and teaser
- Cache per page with article list tag
- Access: view published content permission
## Code Standards
- Use #[Block] attribute (not annotation)
- Inject entity_type.manager and date.formatter
- Use ContainerFactoryPluginInterface
- Include config schema
## Similar Files
- modules/custom/my_articles/src/Plugin/Block/FeaturedArticleBlock.php
## Expected Output
- src/Plugin/Block/RecentArticlesBlock.php
- config/schema/my_articles.schema.yml (update)
Based on the Drupal AI CodeGenerator pattern , this methodology breaks complex tasks into deterministic steps:
Determine what type of task is being requested:
| Type | Description | Approach |
|---|---|---|
| Create | New file/component needed | Generate with DCG, then customize |
| Edit | Modify existing code | Read first, then targeted changes |
| Information | Question about code/architecture | Search and explain |
| Composite | Multiple steps needed | Break down, execute sequentially |
Before generating, verify:
✓ Required dependencies available?
✓ Target directory exists and is writable?
✓ No conflicting files/classes?
✓ All referenced services/classes exist?
✓ Compatible with Drupal version?
Use DCG to scaffold, then customize. This ensures Drupal best practices:
# 1. Generate base structure
drush generate plugin:block --answers='{
"module": "my_module",
"plugin_id": "recent_articles",
"admin_label": "Recent Articles",
"class": "RecentArticlesBlock"
}'
# 2. Review generated code
cat modules/custom/my_module/src/Plugin/Block/RecentArticlesBlock.php
# 3. Customize with specific requirements
# (AI edits the generated file to add business logic)
Always generate tests alongside code:
# Generate kernel test for the new functionality
drush generate test:kernel --answers='{
"module": "my_module",
"class": "RecentArticlesBlockTest"
}'
Expect 80% completion from AI-generated code. Plan for refinement cycles.
┌─────────────────────────────────────────────────────────────┐
│ 1. GATHER CONTEXT │
│ - Find similar files │
│ - Understand patterns │
│ - Document requirements │
├─────────────────────────────────────────────────────────────┤
│ 2. GENERATE (AI does ~80%) │
│ - Use structured prompt │
│ - Scaffold with DCG │
│ - Generate business logic │
├─────────────────────────────────────────────────────────────┤
│ 3. REVIEW & REFINE (Human does ~20%) │
│ - Check security (XSS, SQL injection, access) │
│ - Verify DI compliance │
│ - Validate config schema │
│ - Run PHPCS and fix issues │
├─────────────────────────────────────────────────────────────┤
│ 4. TEST │
│ - Run generated tests │
│ - Add edge case tests │
│ - Manual smoke testing │
├─────────────────────────────────────────────────────────────┤
│ 5. ITERATE (if needed) │
│ - Fix failing tests │
│ - Address review feedback │
│ - Refine based on testing │
└─────────────────────────────────────────────────────────────┘
| Issue | Solution |
|---|---|
| PHPCS errors | Run phpcbf for auto-fix, manual fix for complex issues |
| Missing DI | Add to constructor, update create() method |
| No cache metadata | Add #cache with tags, contexts, max-age |
| Missing access check | Add permission check or access handler |
| No config schema | Create schema file matching config structure |
| Hardcoded strings | Wrap in $this->t() with proper placeholders |
When the AI module is available , leverage drush aigen for rapid prototyping:
# Check if AI Generation is available
drush pm:list --filter=ai_generation
# Generate a complete content type
drush aigen "Create a content type called 'Event' with fields: title, date (datetime), location (text), description (formatted text), image (media reference)"
# Generate a view
drush aigen "Create a view showing upcoming events sorted by date with a calendar display"
# Generate a custom module
drush aigen "Create a module that sends email notifications when new events are created"
Important: Always review AI-generated code. The AI Generation module is experimental and intended for development only.
Create a content type for [purpose].
Content type:
- Machine name: [machine_name]
- Label: [Human Label]
- Description: [Description]
- Publishing options: published by default, create new revision
- Display author and date: no
Fields:
1. [field_name] ([field_type]): [description] - [required/optional]
2. [field_name] ([field_type]): [description] - [required/optional]
After creation, export config with: drush cex -y
Create a service for [purpose].
Service:
- Name: [module].service_name
- Class: Drupal\[module]\[ServiceClass]
- Inject: [service1], [service2]
Methods:
- methodName(params): return_type - [description]
- methodName(params): return_type - [description]
Include:
- Interface definition
- services.yml entry
- PHPDoc with @param and @return
Create an event subscriber for [purpose].
Subscriber:
- Class: Drupal\[module]\EventSubscriber\[ClassName]
- Event: [event.name]
- Priority: [0-100]
Behavior:
- [Describe what should happen when event fires]
Include:
- services.yml entry with tags
- Proper type hints
When generated code doesn't work:
# 1. Check for PHP syntax errors
php -l modules/custom/my_module/src/MyClass.php
# 2. Clear all caches
drush cr
# 3. Check service container
drush devel:services | grep my_module
# 4. Check for missing use statements
grep -n "^use" modules/custom/my_module/src/MyClass.php
# 5. Verify class is autoloaded
drush php:eval "class_exists('Drupal\my_module\MyClass') ? print 'Found' : print 'Not found';"
# 6. Check logs
drush ws --severity=error --count=20
Weekly Installs
223
Repository
GitHub Stars
34
First Seen
Jan 25, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
opencode201
codex195
gemini-cli194
github-copilot188
amp177
kimi-cli177
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
109,600 周安装
image - Image uploadfile - File uploadentity_reference - Reference to other entitieslist_string - Select listtelephone - Phone numberlink_default - URL inputimage_image - Image uploadfile_generic - File uploadentity_reference_autocomplete - Autocomplete referenceblock_plugincontrollersettings_formcontroller | module, class, services |
form-simple | module, class, form_id, route, route_path, route_title, route_permission, link |
form-config | module, class, form_id, route, route_path, route_title |
plugin:block | module, plugin_id, admin_label, category, class, services, configurable, access |
service | module, service_name, class, services |
event-subscriber | module, class, event |