1 февраля 2016

Form API в Drupal 7

Евгений Тристамов
Разработчик

Давайте рассмотрим работу с формами поэтапно, на примере собственного модуля.

Создадим модуль под названием azimut7_forms, который создаст единственную страницу с адресом /azimut7_forms доступную для всех посетителей.

azimut7_forms.info:

name = Azimut7 forms.
description = Drupal Form API examples.
core=7.x
version = "7.x-1.0"
project = "azimut7_forms"
datestamp = "1332419400"

azimut7_forms.module:

/**
 * Implements hook_menu().
 **/
function azimut7_forms_menu(){
  $items = array();

  $items['azimut7_forms'] = array(
    'title' => 'Page title',
    'page callback' => 'azimut7_forms_callback',
    'type' => MENU_NORMAL_ITEM,
    'access callback' => TRUE, 
  );

  return $items;
}

function azimut7_forms_callback() {
  return 'My forms';
}
 

Включаем модуль и переходим на адрес /azimut7_forms. Если вы всё сделали верно, то увидите страницу:

Теперь создадим функцию, в которой будет храниться описание формы:

function azimut7_first_form($form, &$form_state){
  $form = array();
  $form['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Your name'),
    '#default_value' => t('Tomas'),
  );
  $form['settings'] = array(
    '#type' => 'radios',
    '#title' => t('Your sex'),
    '#options' => array(0 => t('Man'), 1 => t('Woman')),
    '#description' => t('Select your sex.'),
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  return $form;
}

В этой функции я задал элемент формы типа textfield — это обычное поле ввода, элемент типа radios — это группа переключателей, и кнопку отправки формы (Submit). Имена ключей массива $form будут использованы как имена элементов формы.

Итак, у нас есть форма с идентификатором azimut7_first_form (ID формы).

Теперь в нашей функции отрисовки страницы azimut7_forms_callback можно вывести форму.

$form_1 = drupal_get_form('azimut7_first_form');
$form_1 = drupal_render($form_1);

Переменную $form_1 можно возвращать в функции azimut7_forms_callback. Форма будет выведена на экран.

Проверяем. Форма получилась, все элементы рабочие. Теперь нам нужен обработчик формы. Для этого создадим функцию с именем:  <идентификатор формы>_submit

В нашем случае это будет:

function azimut7_first_form_submit($form, &$form_state){
...	
}

И сразу добавим валидатор формы. Он создаётся аналогично обработчику, только имя функции оканчивается на validate.

function azimut7_first_form_validate($form, &$form_state){
...
}

Чтобы проверить, работают ли валидатор и обработчик формы, давайте установим модуль Devel и проверим данные формы.

Установка модуля через Drush:

drush dl devel && drush en -y devel

Отобразим вывод значений формы: 

function azimut7_first_form_validate($form, &$form_state){
  dpm($form_state['values']);
}

function azimut7_first_form_submit($form, &$form_state){
  dpm($form_state['values']);
}

Нажимаем Submit. На скриншоте видно, что данные попали в обе функции.

В обработчике можно сохранить данные. Давайте сделаем это и сразу выведем сообщение:

function azimut7_first_form_submit($form, &$form_state){
  variable_set('my_name', $form_state['values']['name']);
  variable_set('my_sex', $form_state['values']['sex']);
  drupal_set_message(t('Your data saved'));
}

Валидация

Далее, можно сделать проверку на длину имени. Предположим, нам подходят имена, состоящие не менее, чем из трёх символов.

Для этого введём проверку в валидаторе. В функции form_set_error первый аргумент — элемент формы, который надо подсветить, а второй — текст ошибки.

function azimut7_first_form_validate($form, &$form_state){
  if (drupal_strlen($form_state['values']['name']) < 3) {
    form_set_error('name', t('Your name is very short.'));
  }
}

Обязательные элементы

Поле для выбора пола у нас необязательное. Давайте исправим эту ситуацию. Для этого нужно добавить элемент #required в соответсвующий элемент формы.

$form['sex'] = array(
  '#type' => 'radios',
  '#title' => t('Your sex'),
  '#options' => array(0 => t('Man'), 1 => t('Woman')),
  '#description' => t('Select your sex.'),
  '#required' => TRUE,
);

Теперь Друпал будет проверять заполнено ли поле или нет.

Собственные обработчики и валидаторы формы

Если вам нужен ещё один валидатор или обработчик, вы можете добавить их. Для этого в форму нужно добавить имена этих обработчиков:

$form['#submit'] = array('azimut7_first_form_submit', 'my_custom_submitter');
$form['#validate'] = array('azimut7_first_form_validate', 'my_sex_validation');

И объявить эти функции:

function my_custom_submitter($form, &$form_state){

}

function my_sex_validation($form, &$form_state){

}

Они тоже будут вызваны при отправке формы. В валидаторе можно проверить, что пол выбран как 1 или 0.

function my_sex_validation($form, &$form_state){
  if (!in_array($form_state['values']['sex'], array(0, 1))) {
    form_set_error('sex', t('Incorrect sex.'));
  }
}

А в обработчике давайте просто выведем сообщение:

function my_custom_submitter($form, &$form_state){
  drupal_set_message(t('It`s custom submitter'));
}

Значения полей формы

Теперь давайте сделаем так, чтобы при открытии этой формы были видны ранее сохранённые данные. Для этого поработаем с #default_value элементов формы.

$form['name'] = array(
  '#type' => 'textfield',
  '#title' => t('Your name'),
  '#default_value' => (variable_get('my_name', '') != '') ? variable_get('my_name', '') : t('Tomas')
);
$form['sex'] = array(
  '#type' => 'radios',
  '#title' => t('Your sex'),
  '#default_value' => (variable_get('my_sex', '') != '') ? variable_get('my_sex', '') : null,
  '#options' => array(0 => t('Man'), 1 => t('Woman')),
  '#description' => t('Select your sex.'),
  '#required' => TRUE,
);

При открытии страницы наша форма уже заполнена:

Шапка и подвал для формы

Для этого добавим элементы #prefix и #suffix прямо к форме.

$form['#prefix'] = '<SOME_TAG>';
$form['#suffix'] = '<SOME_TAG>';

Можно оборачивать все элементы формы. Например, давайте сделаем линию после выбора пола:

$form['sex'] = array(
  '#type' => 'radios',
  '#title' => t('Your sex'),
  '#default_value' => (variable_get('my_sex', '') != '') ? variable_get('my_sex', '') : null,
  '#options' => array(0 => t('Man'), 1 => t('Woman')),
  '#description' => t('Select your sex.'),
  '#required' => TRUE,
  '#suffix' => '<hr>'
);

Отправка формы при помощи Ajax

Чтобы форма отправляла данные через Ajax (без перезагрузки всей страницы) — ей нужно указать специальные обработчики. Добавляются они к элементу Submit:

$form['submit'] = array(
  '#type' => 'submit',
  '#value' => t('Submit'),
  '#ajax' => array(
    'callback' => 'azimut7_ajax_callback',
    'wrapper' => 'my-form',
    'method' => 'replace',
    'effect' => 'fade',
  ),
);

Функция azimut7_ajax_callback будет обработчиком формы. Её вызов произойдёт после вызова всех стандартных обработчиков. И значение, которое она вернёт, заменит элемент с id="my-form", то есть с тем, который мы указали в префиксе всей формы.

Cоздаем функцию azimut7_ajax_callback:

function azimut7_ajax_callback($form, $form_state) {
  return t('Your data saved.');
}

Отправляем форму и видим, что содержимое всей формы меняется:

Оформление формы

Рассмотрим ситуацию, когда у вас уже есть какая-то сложная вёрстка формы и вам нужно её применить. Для этого нужен хук hook_theme:

function azimut7_forms_theme() {
  return array('azimut7_first_form' => array(
    'render element' => 'form',
    'path' => drupal_get_path('module', 'azimut7_forms') . '/templates',
    'template' => 'azimut7-first-form',
  ),
  );
}

Здёсь я объявил, что буду темизировать (оформлять) форму с id = azimut7_first_form. Имя шаблона будет azimut7-first-form.tpl.php и он расположен  в папке модуля azimut7_forms, в подпапке templates.

Создадим папку templates в модуле azimut7_forms и в ней файл azimut7-first-form.tpl.php

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

В шаблоне можно вывести все поля формы по-отдельности. Предположим, у меня есть табличная вёрстка. В коде шаблона это будет выглядеть так:

<?php
  $form["name"]["#title"] = "";
?>
<div class="parent-div">
  <table>
    <tr>
      <td>Your name, please</td>
      <td colspan="2"><?php print render($form["name"]); ?></td>
    </tr>
    <tr>
      <td colspan="3"><?php print render($form["sex"]); ?></td>
    </tr>
    <tr>
      <td></td>
      <td colspan="2"><?php print drupal_render_children($form); ?></td>
    </tr>
  </table>
</div>

Что я сделал: в начале я удалил заголовок поля и, слегка видоизменённым, вывел его в другом месте. А затем раскидал по ячейкам таблицы элементы формы.