ShvetsGroup

 

Captions

RSS Следите за нашим блогом и будьте в курсе последних новостей.

 

Регистрируем пользователя Drupal по шагам

7 комментариев

Регистрируем пользователя Drupal по шагам

Steps

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

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

Итак, нам нужно собирать дополнительные данные — значит, в дополнение к полям логина и пароля, в форме регистрации потребуются другие, и наша форма регистрации разрастется до неприличных размеров. С большой долей вероятности, пользователь испугается большого количества полей и уйдет, так и не нажав заветную кнопку. В области дизайна пользовательских интерфейсов давно придуман способ решения этой проблемы — многошаговая форма (multistep form). В такой форме пользователь вводит данные не все сразу, а шаг за шагом: таким образом мы не только на каждом шаге показываем разумное количество полей, но и избегаем лишних вопросов, в зависимости от уже имеющихся ответов на предыдущем шаге. Кроме того, пользователь может видеть прогресс заполнения в виде индикатора шагов, что тоже важно («Когда же это закончится?», «Сколько мне осталось еще заполнять?»).

Варианты решения

Многошаговые формы в Drupal 6 чаще всего реализуются 2 способами:

  • Способ, предлагаемый нам ядром (а точнее, Forms API) — отправляемая форма сохраняет промежуточные значения в $form_state['storage'] а затем перестраивается путем установки значения $form_state['rebuild'] в TRUE
  • Недостатком такого подхода является недостаточная гибкость — на всех шагах должна быть одна и та же форма. К тому же со стороны Forms API нам предоставляется только сама форма, и перенос промежуточных значений, а значит все остальное, включая интерфейс, нам придется делать самим.

  • С помощью CTools multistep form wizard от "магистра хаоса" Эрла Майлза
    CTools form wizard ("мастер форм") поставляется в рамках модуля Chaos Tools и является полноценным средством для создания многошаговых форм: разработчик получает инструменты для создания шагов, сохранения и переноса промежуточных значений между шагами, работающий "из коробки" интерфейс, включая кнопки перемещения между шагами, индикатор прогресса. Именно на описании этого решения мы и остановимся.

Волшебник, наколдуй нам форму

Следует отметить, что работа мастера с формами существенно отличается от традиционно принятой в Forms API. В частности, вместо drupal_get_form() внутри мастера используется функция ctools_build_form(). Именно за счет собственной обработки каждая функция генерации формы получает на входе "заготовок" формы с готовыми кнопками перемещения между шагами.

Данные наработки Эрла Майлза вошли в ядро Drupal и в 7 версии мы сможем наблюдать похожую работу с (многошаговыми) формами.

Подробную документацию по CTools form wizard можно найти в самом модуле CTools — либо вручную открыв HTML файл либо, либо воспользовавшись модулем Advanced Help от того же Эрла Майлза. Кроме того, этот мастер хорошо описан на примере в статье Ника Льюиса на английском языке. Для не знающих английского, опишем основные свойства данного решения.

Единая точка входа во все шаги

Для построения пошаговой формы создается общий page callback.
Именно в этом месте мы добавляем обработку общей логики для всех шагов.
У каждого шага есть идентификатор: в точке входа мы определяем текущий шаг и задаем этот идентификатор, который затем становится доступным функциям работающим с формами через $form_state['step'].

Заметим, что большую часть работы делает за нас все же мастер форм. Мы подготавливаем параметры нашего мастера форм в массиве, который передаем в качестве аргумента функции ctools_wizard_multistep_form(), внутри которой и происходит вся "магия".

Свой собственный кеш форм

Ядро Drupal ошибочно хранит данные форм в обычном кеше. Кеш, по определению — временные данные, которые с легкостью можно потерять и перестроить с нуля. Однако, данные форм терять недопустимо, поэтому Эрл Майлз создал в модуле CTools свой, более надежный вариант кеша, хранящий объекты в базе в сериализованном виде — CTools object cache (cм. "путь_к_модулю_ctools/includes/object-cache.inc"). Для передачи значений между шагами автором CTools рекомендуется использовать именно его.

У читателя может возникнуть вопрос: «так кэш Drupal тоже хранится в базе?»
Дело в том, что Drupal позволяет перенести кеш в оперативную память, например, с помощью Memcache, при этом надежность хранения данных снизится, в то время как CTools кеш объектов продолжит работу в базе при любой конфигурации стандартного кеша Drupal.

Каждому шагу — своя, уникальная форма

Каждый шаг обрабатывается свой функцией генерации формы. Таким образом, форма каждого шага получает уникальный идентификатор, собственные submit и validation callback-и, в которых можно описывать общую или индивидуальную логику работы шага. В качества бонуса к этому свойству идет легкая структуризация кода шагов — каждому шагу можно выделить отдельный include-файл.

Примеры

Предположим, что мы уже создали в hook_menu() элемент для нашего мастера. Рассмотрим page callback этого элемента (та самая единая точка входа):

function mymodule_signup_wizard($step) {
  // Подключаем код CTools form wizard и CTools object cache.
  ctools_include('wizard');
  ctools_include('object-cache');

  // Конфигурация мастера. Полный набор параметров лучше смотреть в
  // документации.
  $form_info = array(
    // Идентификатор мастера.
    'id' => 'my_signup',
    // Путь к страницам мастера (текущий шаг является переменным
    // аргументом). Должен быть аналогичен путю, заданному в hook_menu().
    'path' => "user/join/%step",
    // Показывать или нет прогресс прохождения шагов
    'show trail' => TRUE,
    // Управление видимостью доп. кнопок перехода.
    'show back' => FALSE,
    'show cancel' => FALSE,
    'show return' => FALSE,
    // Тексты кнопок
    'next text' => t('Next'),
    // Спец. функции, вызываемые мастером
    // - при переходе на след. шаг
    'next callback' =>  'mymodule_wizard_next',
    // - при завершении последнего шага
    'finish callback' => 'mymodule_wizard_finish',
    // Массив, описывающий идентификаторы и названия шагов,
    // а также их порядок.
    'order' => array(
      '1' => t('Welcome'),
      '2' => t('Profile'),
      '3' => t('Finish'),
    ),
    // Массив, где задаются параметры каждой формы (формы каждого шага)
    // В form_id задается идентификатор формы, который является
    // одновременно и названием функции построения формы.
    // Указанные form_id также используются для определения имени функции
    // для текущего шага:
    // - для проверки используется функция $form_id . '_validate',
    // - для отправки — $form_id . '_submit'.
    // Здесь же можно задать дополнительные параметры, например,
    // include файлы, содержащие код каждого шага (полный список 
    // параметров см. в документации).
    'forms' => array(
      '1' => array(
        'form id' => 'mymodule_step1',
      ),
      '2' => array(
        'form id' => 'mymodule_step2',
      ),
      '3' => array(
        'form id' => 'mymodule_step3',
      ),
    ),
  );

  // Пример работы с кешем: передача информации между шагами осуществляется именно здесь.
  // Эрл Майлз рекомендует написать обертку вокруг CTools кеша объектов — 
  // функции mymodule_cache_get() и mymodule_cache_set().
  // В данном примере предполагается, что мы храним данные, передаваемые
  // между шагами, внутри объекта $signup.
  $signup = mymodule_cache_get();
  if (!$signup) {
    // Устанавливаем шаг = 1 — у нас нет данных из кеша.
    $step = current(array_keys($form_info['order']));
    $signup = new stdClass();
    mymodule_cache_set($signup);
  }
  // Таким образом данные из кеша доступны внутри каждого шага внутри
  // массива $form_state.
  $form_state['signup_object'] = $signup;

  // Генерируем вывод формы для текущего шага.
  $output = ctools_wizard_multistep_form($form_info, $step, $form_state);

  return $output;
}

Стоит отметить, что мы можем как угодно манипулировать массивом $form_info — например, менять доступные кнопки и их тексты в зависимости от шага.

Пример кода для 1 шага:


/**
 * Генерация формы.
 * Отличие от обычных функций генерации форм в том, что мы не возвращаем
 * массив формы, а изменяем уже готовую, переданную по ссылке форму.
 * Заметим, что $form уже содержит кнопки для
 * текущего шага, благодаря мастеру.
 */
function mymodule_step1(&$form, &$form_state) {
  $form['age'] = array(
    '#type' => 'textfield',
    '#title' => t('Please enter your age'),
  );
}

/**
 * Проверяем значения формы — здесь ничего особенного по сравнению с
 * обычным Forms API
 */
function mymodule_step1_validate(&$form, &$form_state) {
  if ($form_state['values']['age'] age = $form_state['values']['age'];
}

Мастер регистрации на примере

Опишу нюансы, возникшие при реализации пошаговой регистрации для одного из наших клиентов. Была задача зарегистрировать пользователя, при этом собрать определенное количество персональной информации, создать профиль на основе модуля Content Profile и сохранить собранные данные в профиле, а так же предложить пользователю пригласить на сайт друзей.

Среди требований были следующие пункты:

  • чтобы войти на сайт, пользователь должен подтвердить владение почтовым ящиком, указанным при регистрации
  • мы должны дать пользователю возможность пропустить все шаги, кроме первого
  • после 1 шага пользователь может покинуть мастер и не возвращаться на него

Отмазка: приведенные куски кода существенно упрощены/сокращены для статьи. Автор не гарантирует их работоспособность при применении метода copy-paste.

Шаг 1. Регистрация

В общем-то, основные действия, касающиеся регистрации, происходят на 1 шаге, но пользователь об этом не знает. Это сделано, чтобы собрать максимум информации.

Конструируем форму:


function mymodule_step1(&$form, &$form_state) {
  $form['mail'] = array(
    '#title' => t('Email'),
    '#type' => 'textfield',
    '#required' => TRUE,
    '#size' => 30,
    '#weight' => 2,
  );
  // В данном случае имя пользователя — value, т.е. не изменяемое
  // пользователем значение, заполняемое случайными данными.
  // Так сделано, потому что использовались модули email registration
  // и realname. Читатель может использовать стандартное решение, где
  // пользователь выбирает себе имя сам.
  $form['name'] = array(
    '#type' => 'value',
    '#value' => user_password(),
  );

  $form['pass'] = array(
    // также можно использовать тип password_confirm, выводящий 
    // два поля с проверкой совпадения
    '#type' => 'password',
    '#title' => t('Password');
    '#required' => TRUE,
    '#size' => 30,
    '#weight' => 3,
  );

  // Добавляем поле профиля. Работу с CCK полями рассмотрим позднее,
  // в описании 2 шага.
  $fields = mymodule_step_fields($form_state['step']);
  mymodule_add_profile_fields($fields, $form, $form_state, FALSE);
}

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


function mymodule_step1_validate(&$form, &$form_state) {
  // Вызываем проверку полей регистрации из ядра
  user_register_validate($form, $form_state);

  // Проверка данных профиля.
  // Обработку CCK полей рассмотрим подробнее в описании шага 2.
  mymodule_profile_validate_fields($form, $form_state);  
}

function mymodule_step1_submit(&$form, &$form_state) {
  ..
  $user_array = array(
    'name' => $form_state['values']['name'],
    'mail' => $form_state['values']['mail'],
    'pass' => $form_state['values']['pass'],
    'init' => $form_state['values']['mail'],
    'status' => 1,
  );

  $account = user_save('', $user_array);
  // На этом этапе учетная запись пользователя создана. Если надо,
  // уведомляем пользователя, создаем запись в watchdog.
  ..

  // Создаем профиль пользователя. Эта функцию рассмотрим позже,
  // в шаге 2.
  mymodule_profile_submit_fields(&$form, &$form_state);
}

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

Поскольку пользователь не имеет возможности войти на сайт до подтверждения e-mail, но при этом должен иметь возможность завершить мастер, возникла задача привязки текущего пользователя к его учетной записи и профилю. Данная проблема была решена путем хранения служебной информации о пользователе в его сессии ($_SESSION).

Эта информация помещается туда на этапе submit формы 1 шага.

Шаг 2. Сбор данных для дополнительных полей профиля.

В процессе реализации мастера выяснилось, что собираемые данные по сложности обработки можно условно разделить на 2 типа:

  • свои собственные поля
  • поля CCK (в нашем случае, профиля Content Profile)

С собственными полями все просто, для тех, кто знает Forms API — создаются свои элементы в форме, и обрабатываются стандартным способом внутри validate и submit функций. С полями CCK все обстоит иначе: они имеют свою логику обработки и проверки введенных данных (каждое поле — свою) как на уровне поля, так и на уровне виджета, которая должна работать и в случае нашего мастера. При этом выяснилось, что написать для каждого поля свою логику с нуля слишком трудозатратно и нерационально. Поэтому в проекте были выработаны 2 способа реализации полей CCK внутри собственных форм:

  1. внутри формы создается свой элемент, имитирующий работу поля.
  2. создается фиктивная форма ноды, из которой копируются нужные поля в нашу форму.

В первом способе данные проверяются с помощью validation callback, куда скопирована логика проверки из кода самого поля, затем проверенные данные внутри submit callback помещаются в объект ноды профиля и сохраняются с помощью node_save(). Способ подходит, если поле простое с точки зрения формы и логики обработки. Решение, в общем-то, стандартное.

Второй способ был подсмотрен в модуле Content Profile User Registration. Можно поспорить, является ли он хаком, но для сложных CCK полей он является гораздо более эффективным, чем первый, поэтому его использование оправдано.

Рассмотрим второй способ. Работа с полями в этом случае сводится к след. действиям:

  1. создаем вспомогательную форму ноды нужного типа и копируем нужные поля в нашу форму
  2. на этапе validation — превращаем $form_state['values'] в объект ноды, и проверяем этот объект с помощью функции content_validate(). Эта функция запустит родную проверку значений каждого поля
  3. после удачной проверки, в зависимости от шага, сохраняем готовый объект как профиль пользователя (в случае несуществующего профиля), или копируем значения полей из $form_state['values'] в новый массив, который передаем в drupal_execute() (в случае, если профиль уже существует).

Перейдем к примерам. Для начала — функция mymodule_add_profile_fields(). Эта функция создает вспомогательную форму ноды и копирует оттуда нужные поля в нашу форму.


/**
 * Модифицированная версия функции content_profile_registration_add_profile_form()
 *
 * $node — объект ноды, из которой копируются поля
 * $fields содержит массив добавляемых CCK полей
 * $type — тип ноды профиля, в нашем случае "profile"
 */
function mymodule_add_profile_fields($fields, &$form, &$form_state, $node = FALSE, $type = 'profile') {
  // В зависимости от шага, в функцию передается существующая нода, либо
  // создается новая.
  if (!$node) {
    $node = array('uid' => 0, 'name' => '', 'type' => $type);
  }

  // Создаем дополнительную форму ноды.
  $node_form = drupal_retrieve_form($type .'_node_form', $form_state, $node);
  drupal_prepare_form($type .'_node_form', $node_form, $form_state);

  $node_form += array('#field_info' => array());
  $form_add = array();

  // Если не добавляются элементы формы не связанные с CCK, копируем
  // только поля CCK.
  if (!in_array('other', $fields)) {
    foreach ($node_form['#field_info'] as $field_name => $info) {
      if (isset($node_form[$field_name])) {
        $form_add[$field_name] = $node_form[$field_name];
      }
    }
    // Копируем группы полей.
    $keys = array_keys($node_form);
    foreach ($keys as $key) {
      if (stristr($key, 'group_')) {
        $form_add[$key] = $node_form[$key];
      }
    }
    // Добавляем заголовок
    $form_add['title'] = $node_form['title'];

    // Устанавливаем эти значения равными значению ноды (из 
    // вспомогательной формы) поскольку это может потребоватьcя для 
    // #ahah callbacks.
    $form_add['#node'] = $node_form['#node'];
    $form_add['type'] = $node_form['type'];
  }
  else {
    foreach (array('uid', 'name', 'author', 'buttons', 'language', '#theme', 'options') as $key) {
      unset($node_form[$key]);
    }
    $form_add = $node_form;
  }

  // Корректируем форму с учетом списка нужных нам полей.
  $all_fields = _content_profile_registration_get_fields($type);
  $all_fields['title'] = 'title';
  $all_fields['other'] = 'other';
  foreach ($all_fields as $field_name => $field_info) {
    if (!in_array($field_name, $fields)) {
      if (module_exists('fieldgroup') && ($group_name = _fieldgroup_field_get_group($type, $field_name))) {
        unset($form_add[$group_name][$field_name]);
        if (count(element_children($form_add[$group_name])) == 0) {
          unset($form_add[$group_name]);
        }
      }
      else {
        unset($form_add[$field_name]);
      }
    }
  }

  // Добавляем новые элементы в форму $form.
  $form += array('#field_info' => array());
  $form['#field_info'] += $node_form['#field_info'];
  $form += $form_add;

  // Эта функция вызывается для перестановки полей — можно ее
  // не использовать, если вы планируете использовать собственный
  // механизм обработки "веса" полей.
  $form['#pre_render'][] = 'content_profile_registration_alter_weights';

  // Обработка порядка ("веса") полей
  $form += array('#content_profile_weights' => array());
  $weight = content_profile_get_settings($type, 'weight') + 1;
  foreach (element_children($form_add) as $key) {
    $form['#content_profile_weights'] += array($key => $weight);
  }

  if (isset($node_form['#attributes']['enctype'])) {
    $form['#attributes']['enctype'] = $node_form['#attributes']['enctype'];
  }
}

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

Функция, проверяющая введенные значения в CCK полях (этап validation):


function mymodule_profile_validate_fields(&$form, &$form_state) {
  // $signup_uid должен содержать user id пользователя — 
  // владельца профиля. В нашем случае мы устанавливали его 
  // на основе данных сессии.
  $signup_uid = ..

  require_once drupal_get_path('module', 'node') .'/node.pages.inc';

  // Легкое движение рук, и никакого мошенства. Превращаем значения формы
  // в объект, который будем проверять с помощью стандартного механизма
  // проверки полей CCK.
  $node = (object)$form_state['values'];
  $node->type = 'profile';

  // Это "костыль", требующийся для обхода хитрого модуля Content
  // Profile. Если на данном этапе профиль пользователя уже существует,
  // и мы подготавливаем объект ноды с id не равному id профиля
  // пользователя, Content Profile думает, что мы пытаемся создать для
  // пользователя второй профиль и осуществляет редирект, чего нам 
  // нужно избежать.
  // В нашем случае профиль существует начиная со 2 шага
  if ($form_state['step'] > 1) {
    $profile = content_profile_load('profile', $signup_uid);
    $node->nid = $profile->nid;
  }

  // Готовим список полей для проверки. Удобно иметь функцию
  // mymodule_step_fields(), возвращающую массив названий CCK полей для
  // добавления, в зависимости от текущего шага.
  $fields = mymodule_step_fields($form_state['step']);

  node_object_prepare($node);

  // Убираем имя пользователя для node_validate если оно есть.
  unset($node->name);

  // Проверяем поля разными функциями, в зависимости от того, нужны ли нам
  // "другие" поля ноды — т.е. нужно ли нам проверять еще какие то 
  // элементы формы, кроме  полей CCK (см. модуль Content Profile User Registration).
  if (in_array('other', $fields)) {
    node_validate($node);
  }
  elseif (module_exists('content')) {
    content_validate($node);
  }

  if ($form_state['step'] == 1) {
    // Первый шаг примечателен тем, что мы уже имеем готовую ноду
    // профиля для сохранения. Мы можем ее сохранить для нашего submit 
    // callback.
    $form_state['signup_temp_profile'] = &$node;

  // На данном этапе мы проверили поля, и можем удалить их из формы, если надо.
    foreach ($fields as $field) {
      // Здесь используется функция из модуля content_profile_user_registration
      // но можно написать свою аналогичную.
      _content_profile_registration_remove_values($field, $form[$field], $form_state);
    }
  }
}

Функция, сохраняющая введенные значения в полях (этап submit):


function mymodule_profile_submit_fields(&$form, &$form_state) {
  // $signup_uid должен содержать user id пользователя - 
  // владельца профиля. В нашем случае мы устанавливали его 
  // на основе данных сессии.
  $signup_uid = ..

  // Есть ли у нас профиль на этом шаге ?
  if ($form_state['step'] > 1) {
    // Обновляем существующий профиль.
    $profile = content_profile_load('profile', $signup_uid);

    module_load_include('inc', 'node', 'node.pages');

    $fields = mymodule_step_fields($form_state['step']);

    // Готовим значения путем копирования значений нужных нам полей в новый массив.
    foreach ($fields as $field) {
      $form_state_new['values'][$field] = $form_state['values'][$field];
    }

    // Эмуляция какого действия (нажатие какой кнопки) производится ?
    // Без этого drupal_execute не знает, что делать с формой.
    $form_state_new['values']['op'] = t('Save');
 
    // Наша версия функции drupal_execute().
    mymodule_drupal_execute('profile_node_form', $form_state_new, $profile);

  }
  else {
    // Мы на 1 шаге.
    // Сохраняем профиль пользователя, который мы приготовили на этапе validate.
    $node = &$form_state['signup_temp_profile'];

    // Устанавливаем заголовок.
    if (empty($node->title) && (!module_exists('auto_nodetitle') || auto_nodetitle_get_setting('profile') != AUTO_NODETITLE_OPTIONAL)) {
      $node->title = $form_state['user']->name;
    }
    // Мы не рассмотрели всю обработку формы, поскольку в статье нас
    // интересует только обработка полей CCK.
    // В нашей версии мастера, на 1 шаге находится форма, при 
    // обработке которой сначала создается учетная запись пользователя
    // и объект пользователя сохраняется в $form_state['user'].
    $node->uid = $form_state['user']->uid;
    $node->name = $form_state['user']->name;

    // Сохранение ноды.
    $node = node_submit($node);

    node_save($node);

    if ($node->nid) {
      watchdog('content', 'Content Profile: added %user %type upon registration.', array('%user' => $node->name, '%type' => 'profile'), WATCHDOG_NOTICE, l(t('view'), "node/$node->nid"));
    }
  }

}

На этапе submit возникает проблема: в форме профиля могут быть обязательные поля, которые мы не заполнили. drupal_execute() не даст обработать форму с незаполненными обязательными полями.

Для решения этой проблемы была написана своя функция mymodule_drupal_execute() — аналог drupal_execute(), который перед обработкой делает все элементы формы необязательными.


function mymodule_drupal_execute($form_id, &$form_state) {
  $args = func_get_args();

  $args[1] = &$form_state;

  $form = call_user_func_array('drupal_retrieve_form', $args);

  $form['#post'] = $form_state['values'];

  drupal_prepare_form($form_id, $form, $form_state);

  // Отключаем обязательность полей.
  _mymodule_dont_require($form);

  drupal_process_form($form_id, $form, $form_state);
}

/**
 * Рекурсивная функция отключения обязательности полей.
 */
function _mymodule_dont_require(&$element) {
  foreach (element_children($element) as $child) {
    _mymodule_dont_require($element[$child]);
  }
  if (isset($element['#required'])) {
    $element['#required'] = FALSE;
  }
}

Может показаться, что количество вспомогательного кода слишком велико, и не оправдывается. Это не совсем так: в итоге, у нас получается код, который позволяет добавить любые CCK поля в форму любого шага, и мы можем изменять список добавляемых полей изменяя лишь функцию mymodule_step_fields().

Экономится гигантское количество времени программиста: за нас по сути все делает CCK и код каждого поля. Остается только реализовать обработку для данных формы, не связанных с профилем.

Среди минусов такого подхода можно указать, что не все поля работают в чужой форме "из коробки". Особо "капризному" полю может потребоваться дополнительная забота, чтобы оно заработало в чужой форме.

Шаг 3. Приглашаем друзей

Пользователю показывается форма приглашения друзей (на основе модуля Invite). Форма конструируется элементарно путем вызова функции invite_form() внутри нашей функции построения формы.


function mymodule_step3(&$form, &$form_state) {
  ..
  $form += invite_form($form_state, 'page');
  ..
}

Таким образом, в форму этого шага попадают все элементы стандартной формы приглашения модуля Invite, и остается только выполнить родные submit и validate callback:


function mymodule_step3_validate(&$form, &$form_state) {
  invite_form_validate($form, $form_state);
}

function mymodule_step3_submit(&$form, &$form_state) {
  invite_form_submit($form, $form_state);
}

Шаг 4. Финиш

«Referral code» — простое поле, обрабатываемое с помощью модуля referral (описывать его в рамках статьи нет смысла).

«How did you hear about us?» — поле профиля, обрабатываемое уже описанным в шаге 2 способом.

Итог

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

В 7 версии Drupal ситуация с полями должна улучшиться с приходом более технически совершенного Field API, а работа с формами в целом усовершенствуется благодаря приему наработок Эрла Майлза в ядро.

Комментарии

podarok
23 июля, 2010

Дякую, Саша, за зібраний ві одному місці звязок між cTools & FormsAPI

tornadoxxxl
24 июля, 2010

Давно хотел почитать про возможности CTools.
Спасибо! Хорошая статья!

andypost
24 июля, 2010

Великолепный пример и, пожалуй, первая статья на русском о работе с ctools!

KoDo
6 августа, 2010

Уже более 2х недель посту, а я совсем случайно на него наткнулся, жаль, что не было анонса на Друпал.ру
Давно интересовался что за зверь этот cTools. Спасибо за умную и грамотную статью!

Борис
3 ноября, 2010

Весьма интересно, спасибо!

Виталий
22 декабря, 2010

Спасибо за подробный разбор.
А можете подробнее о mymodule_cache_get() и mymodule_cache_set() написать? Из хелпа ctools не очень понятно...

Виталий
27 декабря, 2010

не хватает в примерах: mymodule_wizard_next(), mymodule_wizard_finish(), mymodule_cache_get(), mymodule_cache_set(), mymodule_step2() ссылки на некоторые есть...

Хотите что-то добавить?