ShvetsGroup

 

10 шагов к постижению форм в Друпале

  • neochief's picture
0 comments

10 шагов к постижению форм в Друпале

Статья эвакуирована с DrupalDance.com

Этот урок создан специально для начинающих и средне-продвинутых Друпал-разработчиков. Он должен быстро дать понятие об азах Forms API, а также показать возможность создаия более сложных вещей на примере пошаговых форм (№8).

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

И прежде чем начать, я расскажу вам как все-таки заставить любой из этих кусков кода работать. Предположим, вы уже имеете установленный тестовый сайт на Друпал 6. Вам прийдется проделать следующие действия:

  1. Создать новую директорию в sites/all/modules, например my_module
  2. Создать файл my_module.info в директории my_module, содержащий это:
  3. name = My module
    description = Module for form api tutorial
    core = 6.x
  4. Создать файл my_module.module. Полностью скопировать отсюда первый пример и вставить в my_module.module.
  5. Включить модуль "My module" на странице модулей (admin/build/modules).
  6. Перейти на страницу my_module/form для запуска кода.
  7. Далее вам предстоит провести для каждого примера, полную замену содержимого my_module.module на код последующего примера. Не забывайте после этого переходить на страницу my_module/form для того, чтобы увидеть результаты своей работы.

Пример №1:

Начнем с самой простой формы:

 'My form',
    'page callback' => 'my_module_form',
    'access arguments' => array('access content'),
    'description' => 'My form',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

// Эта функция вызывается каждый раз, когда мы посещаем страницу 'my_module/form'.
// Функция генерирует и возвращает нашу форму.
function my_module_form() {

  // Форма конструируется при помощи функции drupal_get_form(),
  // в которую нам нужно передать название "функции-строителя" формы.
  return drupal_get_form('my_module_my_form');

}

// Функция-строитель нашей формы.
// Notice it takes one argument, the $form_state
function my_module_my_form($form_state) {
	
  // Наш первый элемент формы — тестовое поле с заголовком "Name".
  // Обратите внимание, что 'Name' обернуто в функцию t(). Это
  // обеспечит дальнейший перевод слова 'Name', чтобы, например,
  // при включенной русской локализации поле называлось 'Имя'.
  $form['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Name'),
  );
  return $form;
}

Пример №2:

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

 'My form',
    'page callback' => 'my_module_form',
    'access arguments' => array('access content'),
    'description' => 'My form',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

function my_module_form() {
  return drupal_get_form('my_module_my_form');
}

function my_module_my_form($form_state) {
  $form['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Name'),
  );
  
  // Добавим в форму простую кнопку отправки. Обратите внимание на то,
  // что при нажатии на кнопку, вы вернетесь обратно на форму, а все ее
  // поля будут очищены. Это стандартное поведение форм.
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  return $form;
}

Пример №3:

Демонстрация набора полей (Fieldsets).

 'My form',
    'page callback' => 'my_module_form',
    'access arguments' => array('access content'),
    'description' => 'My form',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

function my_module_form() {
  return drupal_get_form('my_module_my_form');
}

function my_module_my_form($form_state) {
	
  // Мы создаем элемент "набор полей" и помещаем в него два текстовых
  // поля — для имени, фамилии и отчества.
  //
  // При внимательном рассмотрении этого кода, вы можете заметить, что имя, фамилия
  //  и отчество объявляются как под-масссивы внутри $form['name']. Это говорит друпалу, 
  // что эти элементы нужно поместить внутрь набора полей 'Name'.
  $form['name'] = array(
    '#type' => 'fieldset',
    '#title' => t('Name'),
  );
  $form['name']['first'] = array(
    '#type' => 'textfield',
    '#title' => t('First name'),
  );
  $form['name']['middle'] = array(
    '#type' => 'textfield',
    '#title' => t('Middle name'),
  );
  $form['name']['last'] = array(
    '#type' => 'textfield',
    '#title' => t('Last name'),
  );
  

  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  return $form;
}

Пример №4:

Демонстрация распахивающегося набора полей и базовой валидации обязательных полей.

 'My form',
    'page callback' => 'my_module_form',
    'access arguments' => array('access content'),
    'description' => 'My form',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

function my_module_form() {
  return drupal_get_form('my_module_my_form');
}

function my_module_my_form($form_state) {
	
  // Делаем набор полей распахивающимся
  $form['name'] = array(
    '#type' => 'fieldset',
    '#title' => t('Name'),
    '#collapsible' => TRUE, // распахивающийся
    '#collapsed' => FALSE,  // и не схопнутый по-умолчанию
  );

  // Делаем эти поля обязательными
  $form['name']['first'] = array(
    '#type' => 'textfield',
    '#title' => t('First name'),
    '#required' => TRUE, // добавлено обязательное заполнение
  );
  $form['name']['middle'] = array(
    '#type' => 'textfield',
    '#title' => t('Middle name'),
    '#required' => TRUE, // добавлено обязательное заполнение
  );
  $form['name']['last'] = array(
    '#type' => 'textfield',
    '#title' => t('Last name'),
    '#required' => TRUE, // добавлено обязательное заполнение
  );
  
  
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  return $form;
}

Пример №5:

Демонстрация добавления дополнительных аттрибутов к элементам формы.

 'My form',
    'page callback' => 'my_module_form',
    'access arguments' => array('access content'),
    'description' => 'My form',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

function my_module_form() {
  return drupal_get_form('my_module_my_form');
}

function my_module_my_form($form_state) {
  $form['name'] = array(
    '#type' => 'fieldset',
    '#title' => t('Name'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  
  // Демонстрация дополнительных аттрибутов текстовых полей.
  //
  // Полный список элементов форм и их аттрибутов можно глянуть здесь:
  // http://api.drupal.ru/api/file/developer/topics/forms_api_reference.html
  //
  // Обратите внимание, что в аттрибутах типа #description следует
  // стараться использовать английские значения, обернутые в t().
  // Это облегчит вам дальнейшую жизнь, если в иной день вы
  // захотите сделать многоязычную версию сайта. Если же вы уверены, 
  // что локализации не будет, или ваш английский оставляет желать лучшего,
  // совсем не запрещено указывать там значения прямо на русском. 
  // Однако, в этом случае КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО оборачивать их в t().
  $form['name']['first'] = array(
    '#type' => 'textfield',
    '#title' => t('First name'),
    '#required' => TRUE,
    '#default_value' => t("First name"), // добавлено значение по-умолчанию
    '#description' => t("Please enter your first name."), // добавлена подпись
    '#size' => 20, // добавлена ширина поля
    '#maxlength' => 20, // добавлена максимальная длина строки ввода
  );
  $form['name']['middle'] = array(
    '#type' => 'textfield',
    '#title' => t('Middle name'),
    '#required' => TRUE,
  ); 
  $form['name']['last'] = array(
    '#type' => 'textfield',
    '#title' => t('Last name'),
    '#required' => TRUE,
  );


  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  return $form;
}

Пример №6:

Добавление нового элемента и функции валидации формы.

 'My form',
    'page callback' => 'my_module_form',
    'access arguments' => array('access content'),
    'description' => 'My form',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

function my_module_form() {
  return drupal_get_form('my_module_my_form');
}

function my_module_my_form($form_state) {
  $form['name'] = array(
    '#type' => 'fieldset',
    '#title' => t('Name'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  $form['name']['first'] = array(
    '#type' => 'textfield',
    '#title' => t('First name'),
    '#required' => TRUE,
    '#default_value' => t("First name"),
    '#description' => t("Please enter your first name."),
    '#size' => 20,
    '#maxlength' => 20,
  );
  $form['name']['middle'] = array(
    '#type' => 'textfield',
    '#title' => t('Middle name'),
    '#required' => TRUE,
  ); 
  $form['name']['last'] = array(
    '#type' => 'textfield',
    '#title' => t('Last name'),
    '#required' => TRUE,
  );
  
  // Новое поле — год рождения. Мы произведем проверку значения
  // этого поля в функции валидации формы.
  $form['year_of_birth'] = array(
    '#type' => 'textfield',
    '#title' => t('Year of birth'),
    '#description' => t('Format is "YYYY"'),
  ); 
  
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  return $form;
}

// Добавляем функцию валидации формы. В ней мы будем проверять
// значение поля "год рождения", чтобы быть уверенными, что оно
// находится между 1900 и 2000. Если нет, будет выбрасываться ошибка.
//
// Обратите внимание на название функции. Это просто название 
// функции-строителя формы с  '_validate' на конце. Названная таким
// образом функция, будет служить валидатором формы.
function my_module_my_form_validate($form, &$form_state) {
  $year_of_birth = $form_state['values']['year_of_birth'];
  if (!$year_of_birth || ($year_of_birth  2000)) {
    form_set_error('year_of_birth', t('Enter a year between 1900 and 2000.'));
  }
}

Пример №7:

Добавление функции-обработчика формы.

 'My form',
    'page callback' => 'my_module_form',
    'access arguments' => array('access content'),
    'description' => 'My form',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

function my_module_form() {
  return drupal_get_form('my_module_my_form');
}

function my_module_my_form($form_state) {
  $form['name'] = array(
    '#type' => 'fieldset',
    '#title' => t('Name'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  $form['name']['first'] = array(
    '#type' => 'textfield',
    '#title' => t('First name'),
    '#required' => TRUE,
    '#default_value' => t("First name"),
    '#description' => t("Please enter your first name."),
    '#size' => 20,
    '#maxlength' => 20,
  );
  $form['name']['middle'] = array(
    '#type' => 'textfield',
    '#title' => t('Middle name'),
    '#required' => TRUE,
  ); 
  $form['name']['last'] = array(
    '#type' => 'textfield',
    '#title' => t('Last name'),
    '#required' => TRUE,
  );
  
  $form['year_of_birth'] = array(
    '#type' => 'textfield',
    '#title' => t('Year of birth'),
    '#description' => t('Format is "YYYY"'),
  ); 
  
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  return $form;
}

function my_module_my_form_validate($form, &$form_state) {
  $year_of_birth = $form_state['values']['year_of_birth'];
  if (!$year_of_birth || ($year_of_birth  2000)) {
    form_set_error('year_of_birth', t('Enter a year between 1900 and 2000.'));
  }
}

// Правило создание обработчика почти такое же как и в случае с валидатором.
// Отличие только в том, что вместо '_validate', к названию функции-строителя
// нужно добавить '_submit'.
//
// Обычно в обработчике стоит что-то делать с данными формы, (которые поступают
// поступают внутри переменной $form_state), например, сохранить их в базу.
// Но сейчас мы ограничимся только лишь выводом сообщения на экран, чтобы
// вы убедились, что обработчик действительно работает.
function my_module_my_form_submit($form, &$form_state) {
  drupal_set_message(t('The form has been submitted.'));
}

Пример №8:

Создание новой кнопки — сброса формы и демонстрация присваивания кнопке собственного обработчика. Также, мы переместим проверку всех наших полей в главный валидатор формы.

 'My form',
    'page callback' => 'my_module_form',
    'access arguments' => array('access content'),
    'description' => 'My form',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

function my_module_form() {
  return drupal_get_form('my_module_my_form');
}

function my_module_my_form($form_state) {
  $form['name'] = array(
    '#type' => 'fieldset',
    '#title' => t('Name'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  // Мы убрали базовую валидацию (#required) у всех полей, чтобы
  // проверять их только в валидаторе формы.
  $form['name']['first'] = array(
    '#type' => 'textfield',
    '#title' => t('First name'),
    '#default_value' => t("First name"),
    '#description' => t("Please enter your first name."),
    '#size' => 20,
    '#maxlength' => 20,
  );
  $form['name']['middle'] = array(
    '#type' => 'textfield',
    '#title' => t('Middle name'),
  ); 
  $form['name']['last'] = array(
    '#type' => 'textfield',
    '#title' => t('Last name'),
  );
  
  $form['year_of_birth'] = array(
    '#type' => 'textfield',
    '#title' => t('Year of birth'),
    '#description' => t('Format is "YYYY"'),
  ); 
  
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );

  // Добавляем кнопку очистки формы. Свойство #validate приказывает
  // форме использовать особые валидаторы при нажатии данной
  // кнопки, вместо стандартного.
  $form['clear'] = array(
    '#type' => 'submit',
    '#value' => t('Reset form'),
    '#validate' => array('my_module_my_form_clear'),
  );
  
  return $form;
}

// Добавляем проверку наших полей здесь вместо стандартной базовой.
// Теперь, если пользователь не заполнит поле, он получит сообщение
// об ошибке, а незаполеннное поле отметится красным.
// Если бы мы оставили прежний способ проверки значений (#required),
// то получали бы ошибки тогда, когда пользователь жал бы кнопку Reset
// на форме с пустыми значениями. Это потому, что базовые валидаторы
// выполняются перед валидаторами полей и формы.
function my_module_my_form_validate($form, &$form_state) {
  $year_of_birth = $form_state['values']['year_of_birth'];
  $first_name = $form_state['values']['first'];
  $middle_name = $form_state['values']['middle'];
  $last_name = $form_state['values']['last'];
  if (!$first_name) {
    form_set_error('first', t('Please enter your first name.'));
  }
  if (!$middle_name) {
    form_set_error('middle', t('Please enter your middle name.'));
  }
  if (!$last_name) {
    form_set_error('last', t('Please enter your last name.'));
  }
  if (!$year_of_birth || ($year_of_birth  2000)) {
    form_set_error('year_of_birth', t('Enter a year between 1900 and 2000.'));
  }
}

function my_module_my_form_submit($form, &$form_state) {
  drupal_set_message(t('The form has been submitted.'));
}

// Новый валидатор для кнопки Reset. Выставляя здесь значение $form_state['rebuild']
// в TRUE, мы даем указание пропускать обработчик формы. А пропуская обработчик,
// мы выполняем стандартное действией формы — возвращаемся на нее же с пустыми
// значениями (см пример 2).
function my_module_my_form_clear($form, &$form_state) {
  $form_state['rebuild'] = TRUE;
}

Пример №9:

Создаем третью кнопку, которая будет добавлять на форму дополнительный набор полей имен и года рождения (реальное применение примера — в форме семейного положения, нужно добавить данные супруги, если пользователь женат). Валидация будет производится и для дополнительных полей тоже.

 'My form',
    'page callback' => 'my_module_form',
    'access arguments' => array('access content'),
    'description' => 'My form',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

function my_module_form() {
  return drupal_get_form('my_module_my_form');
}

function my_module_my_form($form_state) {
  $form['name'] = array(
    '#type' => 'fieldset',
    '#title' => t('Name'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  
  // Мы изменяем значение по-умолчанию с расчетом на то,
  // что на втором шаге формы здесь должны быть ранее
  // заполненные значения, а не приглашение к заполнению.
  $form['name']['first'] = array(
    '#type' => 'textfield',
    '#title' => t('First name'),
    '#default_value' => $form_state['values']['first'], // изменено
    '#description' => t("Please enter your first name."),
    '#size' => 20,
    '#maxlength' => 20,
  );
  $form['name']['middle'] = array(
    '#type' => 'textfield',
    '#title' => t('Middle name'),
    '#default_value' => $form_state['values']['middle'], // добавлено
  ); 
  $form['name']['last'] = array(
    '#type' => 'textfield',
    '#title' => t('Last name'),
    '#default_value' => $form_state['values']['last'], // добавлено
  );
  
  $form['year_of_birth'] = array(
    '#type' => 'textfield',
    '#title' => t('Year of birth'),
    '#description' => t('Format is "YYYY"'),
    '#default_value' => $form_state['values']['year_of_birth'], // добавлено
  ); 

  
  // Добавляем новые поля на форму
  if (isset($form_state['storage']['new_name'])) { // Это поле заполняется 
                                                   // при нажатии кнопки "Add new name"
    $form['name2'] = array(
      '#type' => 'fieldset',
      '#title' => t('Name #2'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
    );
    $form['name2']['first2'] = array(
      '#type' => 'textfield',
      '#title' => t('First name'),
      '#description' => t('Please enter your first name.'),
      '#size' => 20,
      '#maxlength' => 20,
      '#default_value' => $form_state['values']['first2'],
    );
    $form['name2']['middle2'] = array(
      '#type' => 'textfield',
      '#title' => t('Middle name'),
      '#default_value' => $form_state['values']['middle2'],
    ); 
    $form['name2']['last2'] = array(
      '#type' => 'textfield',
      '#title' => t('Last name'),
      '#default_value' => $form_state['values']['last2'],
    );
    $form['year_of_birth2'] = array(
      '#type' => 'textfield',
      '#title' => t('Year of birth'),
      '#description' => t('Format is "YYYY"'),
      '#default_value' => $form_state['values']['year_of_birth2'],
    );
  }
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  $form['clear'] = array(
    '#type' => 'submit',
    '#value' => t('Reset form'),
    '#validate' => array('my_module_my_form_clear'),
  );
  
  // Мы добавляем кнопку "Add another name" только если она еще
  // не была кликнута (т.е. на первом шаге). У кнопкий свой валидатор.
  if (empty($form_state['storage']['new_name'])) {  // Это поле заполняется 
                                                    // при нажатии кнопки "Add new name"
    $form['new_name'] = array(
      '#type' => 'submit',
      '#value' => t('Add another name'),
      '#validate' => array('my_module_my_form_new_name'),
    );
  }
  
  return $form;
}

// В этом валидаторе мы выставляем значение $form_state['storage']['new_name'],
// чтобы потом в функции-строителе определить, что форма была нажата.
// На этом этапе вы могли уже и заметить, что переменная $form_state
// передается по ссылке, а это означает, что ее изменения будут видны
// и из функции строителя или из обработчика, который обычно выполняется
// после валидатора.
function my_module_my_form_new_name($form, &$form_state) {
  $form_state['storage']['new_name'] = TRUE;
  $form_state['rebuild'] = TRUE; // Как вы помните, выставление этой
                                 // переменной повлечет перестройку формы.
                                 // Однако текущий $form_state будет доступен
                                 // в функции-строителе.
}

function my_module_my_form_clear($form, &$form_state) {
  unset($form_state['values']); // Принудительно очищаем возможные
  unset($form_state['storage']); // значения в памяти.
  $form_state['rebuild'] = TRUE;
}

// Добавляем дополнительную логику проверки новых полей.
function my_module_my_form_validate($form, &$form_state) {
  $year_of_birth = $form_state['values']['year_of_birth'];
  $first_name = $form_state['values']['first'];
  $middle_name = $form_state['values']['middle'];
  $last_name = $form_state['values']['last'];
  if (!$first_name) {
    form_set_error('first', t('Please enter your first name.'));
  }
  if (!$middle_name) {
    form_set_error('middle', t('Please enter your middle name.'));
  }
  if (!$last_name) {
    form_set_error('last', t('Please enter your last name.'));
  }
  if (!$year_of_birth || ($year_of_birth  2000)) {
    form_set_error('year_of_birth', t('Enter a year between 1900 and 2000.'));
  }
  if ($form_state['storage']['new_name']) {
    $year_of_birth = $form_state['values']['year_of_birth2'];
    $first_name = $form_state['values']['first2'];
    $middle_name = $form_state['values']['middle2'];
    $last_name = $form_state['values']['last2'];
    if (!$first_name) {
      form_set_error('first2', t('Please enter your first name.'));
    }
    if (!$middle_name) {
      form_set_error('middle2', t('Please enter your middle name.'));
    }
    if (!$last_name) {
      form_set_error('last2', t('Please enter your last name.'));
    }
    if (!$year_of_birth || ($year_of_birth  2000)) {
      form_set_error('year_of_birth2', t('Enter a year between 1900 and 2000.'));
    }
  }
}

// Если закномментировать вызов unset() ниже, после добавления на
// форму новых элементов и последующей ее отправки, вы заметите,
// что форма не очистится, так как в $form_state['storage'] будет
// присутствовать выставленное ранее значение. Поэтому, нам нужно
// принудительно очистить буфер значений формы.
function my_module_my_form_submit($form, &$form_state) {
  unset($form_state['storage']);
  drupal_set_message(t('The form has been submitted.'));
}

Пример №10:

Тепер сделаем форму по-настоящему пошаговой — с двумя страницами-шагами. Обратите внимание, что пользователь во время работы с формой остается на том же адресе — 'my_module/form', так как по-умолчанию все это работает на POST запросах.

 'My form',
    'page callback' => 'my_module_form',
    'access arguments' => array('access content'),
    'description' => 'My form',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

function my_module_form() {
  return drupal_get_form('my_module_my_form');
}

// Добавляем дополнительную логику в нашу функцию строитель,
// чтобы обеспечить разделение на в две страницы. Мы проверяем
// значение в $form_state['storage'], чтобы узнать какую страницу
// нужно отображать в данный момент.
function my_module_my_form($form_state) {
  // Показываем страницу-2, если выставлено значение $form_state['storage']['page_two']
  if (isset($form_state['storage']['page_two'])) {
    return my_module_my_form_page_two();
  }

  // Страница-1 отображается, если не выставленно значение $form_state['storage']['page_two']
  $form['name'] = array(
    '#type' => 'fieldset',
    '#title' => t('Name'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  $form['name']['first'] = array(
    '#type' => 'textfield',
    '#title' => t('First name'),
    '#default_value' => $form_state['values']['first'],
    '#description' => t("Please enter your first name."),
    '#size' => 20,
    '#maxlength' => 20,
  );
  $form['name']['middle'] = array(
    '#type' => 'textfield',
    '#title' => t('Middle name'),
    '#default_value' => $form_state['values']['middle'],
  ); 
  $form['name']['last'] = array(
    '#type' => 'textfield',
    '#title' => t('Last name'),
    '#default_value' => $form_state['values']['last'],
  );
  $form['year_of_birth'] = array(
    '#type' => 'textfield',
    '#title' => t('Year of birth'),
    '#description' => t('Format is "YYYY"'),
    '#default_value' => $form_state['values']['year_of_birth'],
  ); 
  // Добавляем дополнительные поля имени 
  if (!empty($form_state['storage']['new_name'])) { 
    $form['name2'] = array(
      '#type' => 'fieldset',
      '#title' => t('Name #2'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
    );
    $form['name2']['first2'] = array(
      '#type' => 'textfield',
      '#title' => t('First name'),
      '#description' => t('Please enter your first name.'),
      '#size' => 20,
      '#maxlength' => 20,
      '#default_value' => $form_state['values']['first2'],
    );
    $form['name2']['middle2'] = array(
      '#type' => 'textfield',
      '#title' => t('Middle name'),
      '#default_value' => $form_state['values']['middle2'],
    ); 
    $form['name2']['last2'] = array(
      '#type' => 'textfield',
      '#title' => t('Last name'),
      '#default_value' => $form_state['values']['last2'],
    );
    $form['year_of_birth2'] = array(
      '#type' => 'textfield',
      '#title' => t('Year of birth'),
      '#description' => t('Format is "YYYY"'),
      '#default_value' => $form_state['values']['year_of_birth2'],
    );
  }
  $form['clear'] = array(
    '#type' => 'submit',
    '#value' => t('Reset form'),
    '#validate' => array('my_module_my_form_clear'),
  );
  
  if (empty($form_state['storage']['new_name'])) {
    $form['new_name'] = array(
      '#type' => 'submit',
      '#value' => t('Add another name'),
      '#validate' => array('my_module_my_form_new_name'),
    );
  }
  $form['next'] = array(
    '#type' => 'submit',
    '#value' => t('Next »'),
  );
  return $form;
}

// Новая функция делает код более универсальным и управляемым.
// На втором шаге у нас будет выбор любимого цвета.
function my_module_my_form_page_two() {
  $form['color'] = array(
    '#type' => 'textfield',
    '#title' => t('Favorite color'),
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  return $form;
}

function my_module_my_form_new_name($form, &$form_state) {
  $form_state['storage']['new_name'] = TRUE;
  $form_state['rebuild'] = TRUE; // Пропускаем обработчик.
}

function my_module_my_form_clear($form, &$form_state) {
  unset($form_state['values']); // Принудительно очищаем возможные
  unset($form_state['storage']); // значения в памяти.
  $form_state['rebuild'] = TRUE;
}

// Добавляем валидацию второго шага.
function my_module_my_form_validate($form, &$form_state) {
  // Здесь делаем проверку второй страницы.
  if (isset($form_state['storage']['page_two'])) {
    $color = $form_state['values']['color'];
    if (!$color) {
      form_set_error('color', t('Please enter a color.'));
    }
    return;
  }
  
  $year_of_birth = $form_state['values']['year_of_birth'];
  $first_name = $form_state['values']['first'];
  $middle_name = $form_state['values']['middle'];
  $last_name = $form_state['values']['last'];
  if (!$first_name) {
    form_set_error('first', t('Please enter your first name.'));
  }
  if (!$middle_name) {
    form_set_error('middle', t('Please enter your middle name.'));
  }
  if (!$last_name) {
    form_set_error('last', t('Please enter your last name.'));
  }
  if (!$year_of_birth || ($year_of_birth  2000)) {
    form_set_error('year_of_birth', t('Enter a year between 1900 and 2000.'));
  }
  if ($form_state['storage']['new_name']) {
    $year_of_birth = $form_state['values']['year_of_birth2'];
    $first_name = $form_state['values']['first2'];
    $middle_name = $form_state['values']['middle2'];
    $last_name = $form_state['values']['last2'];
    if (!$first_name) {
      form_set_error('first2', t('Please enter your first name.'));
    }
    if (!$middle_name) {
      form_set_error('middle2', t('Please enter your middle name.'));
    }
    if (!$last_name) {
      form_set_error('last2', t('Please enter your last name.'));
    }
    if (!$year_of_birth || ($year_of_birth  2000)) {
      form_set_error('year_of_birth2', t('Enter a year between 1900 and 2000.'));
    }
  }
}

// Изменяем обработчик так, чтобы он правильно работал в зависимости
// от того, на каком шаге была отправлена форма. Если мы на первом шаге,
// то устанавливаем $form_state['storage']['page_two'], после чего форма
// перегрузится и будет знать, что нужно отображать второй шаг.
// Если же форма была отправлена на втором шаге, то следует показать
// пользователю сообщение об успешном завершении операции и
// переместить его на другую страницу.
function my_module_my_form_submit($form, &$form_state) {
  // Обработка первого шага
  if ($form_state['clicked_button']['#id'] == 'edit-next') {
    $form_state['storage']['page_two'] = TRUE; // Устанавливаем флаг для функции-строителя
                                               
    // Запихиваем значения первого шага в $form_state['storage'],
    // чтобы иметь к ним доступ в финальном обработчике на втором шаге.
    $form_state['storage']['page_one_values'] = $form_state['values'];
  }
  // Второй шаг, финальный обработчик.
  else {
    // Как я уже говорил, на этом этапе обычно производится сохранение
    // данный в базу данных. Но мы ограничимся показом сообщения об
    // успешном завершении операции.    
    drupal_set_message(t('Your form has been submitted'));

    // Это значение должно быть очищено, чтобы редирект состоялся.
    // Дело в том, что если оно не пустое, $form_state['rebuild'] автоматически
    // устанавливается в TRUE (см. первый пункт документации:
    // http://drupal.org/node/144132
    unset ($form_state['storage']); 

    // Отправляем пользователя на главную страницу                               
    $form_state['redirect'] = ''; 
  }
}

Примечания

Я старался сильно не отходить от оригинала, исправляя только мелкие неточности и ошибки. Однако, замечу, что с приходом шестого друпала, нет никакой необходимости пихать все проверки в общий валидатор формы. Для каждого элемента можно определять свою функцию-валидатор. Например:

  $form['year_of_birth'] = array(
    '#type' => 'textfield',
    '#title' => t('Year of birth'),
    '#description' => t('Format is "YYYY"'),
    '#default_value' => $form_state['values']['year_of_birth'],
    '#element_validate' => array('is_valid_year'),
  ); 

  // ...

function is_valid_year($form_element, &$form_state) {
  $year_of_birth = $form_element['#value'];
  if (!$year_of_birth || ($year_of_birth  2000)) {
    form_set_error($form_element['#name'], t('Enter a year between 1900 and 2000.'));
  }
}

Но стоит помнить, что валидаторы элементов выполняются после валидатора формы.

Кроме того, возможно, кто-то спросит, почему не оборачиваются в t() заголовок и описание пункта меню. Отвечаю заранее — в шестом друпале это делается автоматически.

Полезные ссылки

Got anything to add?