logo

额外区块类型 (EBT) - 全新的布局构建器体验❗

额外区块类型 (EBT) - 样式化、可定制的区块类型:幻灯片、标签页、卡片、手风琴等更多类型。内置背景、DOM Box、JavaScript 插件的设置。立即体验布局构建的未来。

演示 EBT 模块 下载 EBT 模块

❗额外段落类型 (EPT) - 全新的 Paragraphs 体验

额外段落类型 (EPT) - 类似的基于 Paragraph 的模块集合。

演示 EPT 模块 滚动

滚动

9.8.1. hook_form_alter() 为现有表单添加提交和验证功能。

17/10/2025, by Ivan

在之前的一节课中,我们已经了解了钩子(hooks)的概念,在本节课中,我们将实践使用 hook_form_alter() 钩子,并向现有表单添加功能。

代码示例可以在 GitHub 上找到:
https://github.com/levmyshkin/drupalbook8

在本节课中,我们将开始实践钩子的使用,稍后我们将回到钩子并查看其他一些示例。现在,让我们从 hook_form_alter() 开始。

为了向模块中添加钩子,我们需要创建一个名为 MODULENAME.module 的文件,因此我们将创建一个名为 drupalbook.module 的文件。这个文件将是一个 PHP 文件,我们的钩子和辅助函数将存储在其中,其余的最好将文件和类放在 src 文件夹中。现在,你只需要在文件中添加一个开头标签:

drupalbook

现在,让我们添加 hook_form_alter()。如果你使用 PhpStorm,开始输入钩子的名称,PhpStorm 会提示你选择一个钩子。当你选择钩子时,PhpStorm 会自动将参数插入到函数中,你无需记住或查阅帮助文档来添加必要的参数:

function

当你想添加一个钩子时,你需要将钩子名称中的 hook 替换为你的模块名称,然后 Drupal 会自动将你的代码插入到钩子位置。最终,你应该有这个函数:

/**
 * Implements hook_form_alter().
 */
function drupalbook_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
 
}

在上一节课中,我们已经讨论过,$form、$form_state 中存储的内容,以及 $form_id 中存储的内容,我们将存储表单 ID,该 ID 在相应的方法中定义:

9.8. 在 Drupal 中使用表单。通过代码添加配置表单。

现在让我们扩展上一节课中添加 API 密钥的表单。为此,我们需要定义表单 ID,你可以在这个方法中看到它:

function getFormId() {
  return 'drupalbook_admin_settings';
}

或者打开 DOM 检查器并查看 HTML 中的 id 属性:

settings

在这里,我们需要将 -(连字符)替换为 _(下划线)。但请小心,表单标签属性中的 ID 可能在 AJAX 请求后递增,例如,drupalbook-admin-settings-0、drupalbook-admin-settings-1、drupalbook-admin-settings-2 等。我们需要去掉结尾的数字,保留从 getFormId() 方法中指定的表单 ID 部分。

现在,我们需要限制我们的代码仅在 drupalbook_admin_settings 表单中执行,因为在 hook_form_alter() 中的代码将会为通过 Drupal 表单 API 生成的所有表单执行:

($form_id == 'drupalbook_admin_settings') {
  // 这里是进一步的代码。
}

在 if 语句中,我们可以编写关于 API 密钥表单的代码。让我们为文本字段添加占位符,以便空字段显示应该填写的内容:

($form_id == 'drupalbook_admin_settings') {
  $form['drupalbook_api_key']['#attributes']['placeholder'] = 'API key';
  $form['drupalbook_api_client_id']['#attributes']['placeholder'] = 'API client ID';
}

要应用 hook_form_alter(),请清理缓存。

api key

虽然 #attributes 没有在文档中列为 textfield 的可能键:

https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21Element%21Textfield.php/class/Textfield/8.6.x

然而,这个键仍然可以用来指定标签的属性:class、placeholder、不同的 rel 属性。对于 Drupal 7 表单 API,有一个更清晰的表格:

https://api.drupal.org/api/drupal/developer%21topics%21forms_api_reference.html/7.x

你可以在这里查看 textfield 字段的 #attributes。

属性值是在生成(渲染)表单元素时通过 Form API 设置的,在 setAttributes 方法中:

https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21Element%21RenderElement.php/function/RenderElement%3A%3AsetAttributes/8.2.x

虽然 API 文档中没有明确说明,但你可以看到它是如何工作的。

你还可以在基本的 FormElement 类中看到可以为表单字段设置的其他值:
https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21Element%21FormElement.php/class/FormElement/8.2.x

Drupal 的 Form API 看起来不太清晰和有些混乱,因此我仍然有些东西没有完全理解。但是,使用这个 API 是足够简单且清晰的,只要手边有一个有效的示例。因此,如果你有任何问题,不知道如何在表单中做到某一部分,请上网查找,你肯定会找到 100% 有效的示例。你需要知道的是,所有表单都会经过 Drupal 的缓存、渲染、主题化等 API,任何表单中的字段都可以通过 hook_form_alter() 来修改,关于如何做,你可以通过 Google 查找答案。此外,你接触 Form API 的次数越多,解析它的示例也会越多,Form API 对你来说就会变得越容易理解。不要担心它如此庞大,当前只需使用你所需要的部分即可。

验证
现在,让我们看一下如何通过 Form API 使用验证功能。我们检查 API 密钥字段,确保它以 "google" 开头,例如 google-KEY123a3sa。如果我们最初编写了表单代码,我们可以插入 validateForm() 方法并在其中进行所有检查:

/**
 * {@inheritdoc}
 */
public function validateForm(array &$form, FormStateInterface $form_state) {
 
}

但通常我们处理的表单都在贡献模块中,最好为这些表单编写自定义模块中的代码。所以我们将向表单添加一个新的验证函数:

['#validate'][] = 'drupalbook_settings_validate';

在 #validate 数组中,我们存储所有回调和验证函数。回调是通过函数名称调用函数,即我们将添加 ['callback_function1', 'callback_function2'] 数组,然后 Drupal 会调用这些函数来检查表单。现在,我们需要在 drupalbook.module 文件中创建 drupalbook_settings_validate 函数。在这个函数中,我们将有 $form 和 $form_state 参数:

/**
 * Custom validation callback.
 */
function drupalbook_settings_validate(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
 
}

现在让我们添加 API 密钥字段本身的检查:

/**
 * Custom validation callback.
 */
function drupalbook_settings_validate(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
  if (strpos($form_state->getValue('drupalbook_api_key'), 'google') === FALSE) {
    $form_state->setErrorByName('drupalbook_api_key', t('API Key must start from "google".'));
  }
}

我们使用严格比较操作符 ===,因为如果 "google" 位于字符串的开头,strpos() 返回 0,而在 PHP 中(0 == FALSE)会返回 TRUE,因为在 PHP 中,''、0、NULL、FALSE 都是空值,它们在简单比较 == 中是相等的。

现在,每次保存设置表单时,都会执行检查,如果未通过检查,Drupal 将生成一个错误,设置不会保存:

settings form

提交
在所有验证函数执行并且没有错误的情况下,Drupal 会调用提交函数,它们会在提交表单数据后执行。你已经在上一节课中看过了 submitForm() 方法,我们在其中保存数据到配置中。但是我们也可以在提交时执行其他操作,例如更改数据或将数据保存到其他实体中。让我们做一个验证函数,当 API 密钥使用时,显示一条额外的消息。在 hook_form_alter() 中,我们将添加函数名称:

['#submit'][] = 'drupalbook_settings_submit';

现在,在 drupalbook_settings_settings_submit() 函数中,我们将传递 $form 和 $form_state 参数,在该函数中显示消息。

/**
 * Custom submit callback.
 */
function drupalbook_settings_submit(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
  \Drupal::messenger()->addStatus(
    t(htmlentities('Insert API key in your  tag:  .'))
  );
}

我们使用 htmlentities() 函数来确保所有用于输出的特殊字符(如 "<"、"/"、">")不会被 Drupal 删除。所有 HTML 文本都通过 t() 函数切割,因此它不能被粘贴到文本中。例如,如果这段代码包含 JavaScript 重定向到另一个站点,显示消息时就会发生重定向。这就是为什么我们使用 htmlentities 函数来显示标签。

当验证函数失败时,消息不会显示,只有在表单没有错误时,我们才能看到消息。

drupalbook form

以前,drupal_set_message 函数用于在 Drupal 中输出消息:

(t('An error occurred and processing did not complete.'));

但现在大家都尽量统一并将 OOP 用到每个地方。

以下是完整的 drupalbook.module 文件当前代码:

/**
 * Implements hook_form_alter().
 */
function drupalbook_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  if ($form_id == 'drupalbook_admin_settings') {
    $form['drupalbook_api_key']['#attributes']['placeholder'] = 'API key';
    $form['drupalbook_api_client_id']['#attributes']['placeholder'] = 'API client ID';
 
    $form['#validate'][] = 'drupalbook_settings_validate';
    $form['#submit'][] = 'drupalbook_settings_submit';
  }
}
 
/**
 * Custom validation callback.
 */
function drupalbook_settings_validate(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
  if (strpos($form_state->getValue('drupalbook_api_key'), 'google') === FALSE) {
    $form_state->setErrorByName('drupalbook_api_key', t('API Key must start from "google".'));
  }
}
 
/**
 * Custom submit callback.
 */
function drupalbook_settings_submit(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
  // drupal_set_message is deprecated
  // drupal_set_message(t('An error occurred and processing did not complete.'));
 
  \Drupal::messenger()->addStatus(
    t(htmlentities('Insert API key in your  tag:  .'))
  );
}

这就是我们关于 Form API 的课程结束了,但这并不是学习 Form API 的终点,我们将在接下来的课程中再次遇到验证、提交以及各种字段的函数。我们还将在其中一节课中了解如何通过 Form API 使用 AJAX 和 form_states。

代码示例可以在 GitHub 上找到:
https://github.com/levmyshkin/drupalbook8