12.8.1. hook_form_alter() добавляем submit и validate для существующей формы.

drupal 8 hook_form_alter()

В одном из прошлых уроков мы разбирали что такое хуки, в этом уроке мы на практике поработаем с хуком hook_form_alter() и подобавляем функционал для уже существующей формы.

Примеры кода можно посмотреть на github:
https://github.com/levmyshkin/drupalbook8

Что такое хук и зачем он нужен можно почитать здесь:

http://drupalbook.ru/drupal/122-chto-takoe-hook-v-drupal-8

В этом уроке мы начнем на практике рассматривать хуки, позже мы еще вернемся к хукам и посмотрим еще пару примеров. А пока давайте начнем с hook_form_alter(). 

Для того чтобы добавить хук в модуль, нужно создать файл MODULENAME.module, у нас название модуля drupalbook, поэтому мы будем создавать файл drupalbook.module. Это будет PHP файл в котором будут храниться наши хуки и вспомогательные функции, для всего остального лучше всего использовать отдельные файлы и классы в папке src. Пока что можете добавить в файл просто открывающийся тег <?php:

drupal 8 hook_form_alter()

Теперь давайте добавим hook_form_alter(). Если вы используете PhpStorm, то начинайте писать название хука и PhpStorm предложит вам выбрать один из хуков. Когда вы выбираете хук таким образом, то PhpStorm  подставляет автоматически аргументы в функцию и вам не нужно запоминать или искать в справке, какие аргументы вам нужно добавлять:

drupal 8 hook_form_alter

Когда вы хотите добавить хук, вам нужно слово hook заменить на имя вашего модуля и тогда друпал автоматически будет подставлять ваш код в место вызова хука.

В результате у вас должна быть вот такая функция:

<?php

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

}

В прошлом уроке мы уже рассматривали, что храниться в $form, $form_state, в $form_id у нас будет храниться id формы, который мы определили в соотвествующем методе:

http://drupalbook.ru/drupal/128-rabota-s-formami-v-drupal-8-dobavlyaem-formu-administrirovaniya 

Давайте теперь расширим форму добавления API ключей, которую мы делали на прошлом уроке. Для этого нам нужно определить id формы, вы можете его посмотреть в этом методе:

  public function getFormId() {
    return 'drupalbook_admin_settings';
  }

Или открыть DOM-инспектор и посмотреть атрибут id в HTML:

drupal 8

Здесь нам нужно заменить - (дефисы) на _ (нижние подчеркивания). Но будьте внимательны, id в атрибуте тега form может быть инкрементировано после AJAX-запроса, например drupalbook-admin-settings-0, drupalbook-admin-settings-1, drupalbook-admin-settings-2 и так далее. Нам нужна часть без числа на конце, иммено эта часть сформированна из id формы, который указан в методе getFormId().

Теперь нам нужно ограничить выполенения нашего кода только для формы drupalbook_admin_settings, потому что код который находится в hook_form_alter(), будет выполняться для абсолютно всех форм, которые сгенерированны через Drupal Form API:

  if ($form_id == 'drupalbook_admin_settings') {
    // Further code here.
  }

Внутри if'а мы можем писать наш код для формы API ключей. Давайте добавим placeholder'ы для текстовых полей, чтобы в пустых полях было видно что нужно писать:

  if ($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() применился, почистите кэш. 

drupal 8 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 Form API:

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

Вы можете найти #attributes здесь для полей типа textfield.

Значения атрибутов устанавливается в Form API при генерации (render'а) элемента формы в методе setAttributes:

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

Хотя в Form 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% найдете работающий пример. Вам нужно знать, что все формы проходят через все API друпала Cache, Render, Theming и любое поле в форме вы можете изменить через hook_form_alter(), а как именно это сделать вы уже можете найти через гугл. Также чем больше вы будете сталкиваться с Form API и чем больше вы будете разбирать примеров с ним, тем проще и понятнее вам будет становиться этот Form API. И не волнуйтесь, что он такой большой, просто используйте только ту часть которая вам нужна сейчас.

Validate

Давайте теперь рассмотрим как использовать validate функции через Form API. Сделаем проверку поля API Key, чтобы оно начиналось со слова google, например google-KEY123a3sa. Если бы мы писали изначально код для нашей формы, мы могли бы вставить validateForm() метод и в нем сделать все проверки:

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

}

Но зачастую формы с которыми мы работаем находятся в контрибных модулях и лучше писать код для таких форм в кастомных модулях. Поэтому давайте добавим новую функцию валидации в форму:
 

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

В массиве #validate мы храним все callback'и функций валидации. Callback - это вызов функции по ее имени, то есть мы сложим массив ['callback_function1', 'callback_function2'], а потом возмем из массива эти названия и друпал вызовет эти функции. В данном случае друпал вызовит эти функции для проверки нашей формы. И теперь нам нужно создать в файле drupalbook.module функцию drupalbook_settings_validate. В этой функции у нас будут параметры $form, $form_state:

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

}

И теперь давайте добавим саму проверку поля API Key:

/**
 * 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 form api

Submit

После того как отработали все validate функции и никаких ошибок не произошло, то друпал вызывает submit функции, они срабатывают уже после отправки данных с формы. Вы уже видели в прошлом уроке метод для submit'а submitForm(), в ней мы сохраняем данные в конфиги. Но мы можем выполнять и другие действия на submit, например изменять данные или сохранять часть данных в другие сущности. Давайте сделаем еще одну функцию валидации, которая будет выводить дополнительное сообщение о том как использовать API Key. В hook_form_alter() мы добавим название функции:

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

Теперь в функции drupalbook_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 <head> tag:  <script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap" type="text/javascript"></script>.'))
  );
}

Мы используем функцию htmlentites(), чтобы все специальные символы, которые мы используем для вывода тегов "<", "/", ">", не удалялись друпалом. Весь HTML текст вырезается из функции t(), это нужно для того чтобы в этот текст нельзя было вставить код, если например такой код содержит javascript редирект на другой сайт, то при выводе сообщений будет проиходить редирект. Поэтому чтобы вывести теги, мы и используем функцию htmlentities.

Сообщение не будет выводиться, если функции валидации не прошли успешно, только когда в форме нет ошибок мы увидем сообщение.

drupal 8 settings

Раньше в drupal использовалась функция drupal_set_message, для вывода сообщений:

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

Но теперь все стараются унифицировать и привести к использованию ООП повсеместно.

Ниже весь текущий код файла drupalbook.module:

<?php

/**
 * 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 <head> tag:  <script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap" type="text/javascript"></script>.'))
  );
}

На этом заканчивается наш урок по Form API, но не заканчивается изучение Form API, мы еще ни раз столкнемся с функциями validate, submit и различными полями. Мы также в одно из уроков разберем как использовать AJAX и form_states через Form API.

Примеры кода можно посмотреть на github:
https://github.com/levmyshkin/drupalbook8