Давайте рассмотрим работу с формами поэтапно, на примере собственного модуля.
Создадим модуль под названием 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>
Что я сделал: в начале я удалил заголовок поля и, слегка видоизменённым, вывел его в другом месте. А затем раскидал по ячейкам таблицы элементы формы.
