pict-program-code.png Drupalではフォームの扱いがちょっと面倒で自分自身苦手だったので、ちょっとおさらいのためにウィザード形式のマルチフォームの簡単なサンプルを作ってみました。

ウィザード形式ですので、前後のページへの移動ボタンなどもちゃんとあります。この記事の元になったdrupal.orgの記事は以下の通りです。

Tutorial: Ten Step-by-Step Code Samples for Learning Form API
http://drupal.org/node/262422

この記事のSample #10がマルチステップのフォームなのですが、これを拡張して前後のページに移動できるようにして、ページ数をふやしたりコードを整理したものが今回のサンプルモジュールです。自分自身の覚え書きの意味もありあえて記事に残すことにしました。

試してみよう

サンプルはmy_moduleという簡単なモジュールにしましたので試してみるにはこのページの下からダウンロードしてインストールしてください。インストール後はこのモジュールを管理ぺージから有効にすれば準備完了です。

テストするにはクリーンURL機能のON/OFFに応じて以下のURLをアクセスします。
クリーンURLがONの場合
 http://(DrupalサイトのURL)/my_module/form
クリーンURLがOFFの場合
 http://(DrupalサイトのURL)/?q=my_module/form


コードの簡単な説明

以下がこのモジュールのコードです。ちょっと長いので分割して説明していきます。
  1. <?php
  2. /**
  3.  * @file
  4.  *   Example of Multistep Form with Drupal 6.x
  5.  *
  6.  * @author
  7.  *   Hideki Ito - PIXTURE STUDIO <http://www.pixture.com>
  8.  */
  9.  
  10. /**
  11.  * Implementation of hook_menu()
  12.  */
  13. function my_module_menu() {
  14.   $items['my_module/form'] = array(
  15.     'title' => t('Multistep Form Sample'),
  16.     'page callback' => 'drupal_get_form',
  17.     'page arguments' => array('my_module_my_form'),
  18.     'access arguments' => array('access content'),
  19.     'description' => t('My multistep form'),
  20.     'type' => MENU_CALLBACK,
  21.   );
  22.   return $items;
  23. }
ここはhook_menu()の実装関数で、ここでメニュー定義をしています。my_module/formが呼び出されると、
drupal_get_form('my_module_my_form')がコールされることになります。

  1. /**
  2.  * Form builder - main
  3.  */
  4. function my_module_my_form($form_state) {
  5.   if (!$form_state['storage']['page']) {
  6.     // directly invoiked from menu
  7.     $form_state['storage']['page'] = 1;
  8.   }
  9.   $page = $form_state['storage']['page'];
  10.  
  11.   $form = array();
  12.   // get page specific portion of the form
  13.   switch ($page) {
  14.     case 1:
  15.       $form = my_module_my_form_page_1($form_state);
  16.       break;
  17.     case 2:
  18.       $form = my_module_my_form_page_2($form_state);
  19.       break;
  20.     case 3:
  21.       $form = my_module_my_form_page_3($form_state);
  22.       break;
  23.     case 4:
  24.       $form = my_module_my_form_page_4($form_state);
  25.       break;
  26.   }
  27.   // add common buttons to the bottom of the form
  28.   if ($page > 1) {
  29.     $form['back'] = array(
  30.       '#type' => 'submit',
  31.       '#value' => '<< Back',
  32.     );
  33.   }
  34.   if ($page < 4) {
  35.     $form['next'] = array(
  36.       '#type' => 'submit',
  37.       '#value' => 'Next >>',
  38.     );
  39.   }
  40.   else { // only on page 4
  41.     $form['submit'] = array(
  42.       '#type' => 'submit',
  43.       '#value' => 'Submit',
  44.     );
  45.   }
  46.   $form['clear'] = array(
  47.     '#type' => 'submit',
  48.     '#value' => 'Reset form',
  49.     '#validate' => array('my_module_my_form_clear'),
  50.   );
  51.   return $form;
  52. }
ここはフォームを生成をするメイン関数です。このモジュールでは、$form_state['storage']['page']というグローバルを使って現在のページがどこかという情報を保持します。ただし、メニューから起動がかかった直後はこのグローバルは設定されていませんので、ページ1として生成をしています。

このページによって各ページ独自のフォーム項目を生成するサブルーチンをコールします。そして、ページ下部にくるボタンは共通化してこの関数の中で定義しています。「Back」ボタンは1ページにはなく、「Next」ボタンは最終ページにはありません。また、最終ページにのみ「Submit」ボタンを置いています。

  1. /**
  2.  * Form builder for page 1
  3.  */
  4. function my_module_my_form_page_1($form_state) {
  5.   $default_name = $form_state['values']['name'];
  6.   $default_country = $form_state['values']['country'];
  7.   // load the stored data
  8.   if (isset($form_state['storage']['page_values_1'])) {
  9.     $default_name = $form_state['storage']['page_values_1']['name'];
  10.     $default_country = $form_state['storage']['page_values_1']['country'];
  11.   }
  12.   $form['title'] = array(
  13.     '#type' => 'markup',
  14.     '#value' => '<h3>Page 1</h3>',
  15.   );
  16.   $form['name'] = array(
  17.     '#type' => 'textfield',
  18.     '#title' => t('Name'),
  19.     '#default_value' => $default_name,
  20.     '#description' => "Please enter your name.",
  21.     '#size' => 20,
  22.     '#maxlength' => 20,
  23.   );
  24.   $form['country'] = array(
  25.     '#type' => 'textfield',
  26.     '#title' => t('Country'),
  27.     '#default_value' => $default_country,
  28.     '#description' => "Please enter your country.",
  29.   );
  30.   return $form;
  31. }
  32.  
  33. /**
  34.  * Form builder for page 2
  35.  */
  36. function my_module_my_form_page_2($form_state) {
  37.   $default_color = $form_state['values']['color'];
  38.   // load the stored data
  39.   if (isset($form_state['storage']['page_values_2'])) {
  40.     $default_color = $form_state['storage']['page_values_2']['color'];
  41.   }
  42.   $form['title'] = array(
  43.     '#type' => 'markup',
  44.     '#value' => '<h3>Page 2</h3>',
  45.   );
  46.   $form['color'] = array(
  47.     '#type' => 'radios',
  48.     '#title' => 'Favorite Color',
  49.     '#options' => array(
  50.       'Blue' => 'Blue',
  51.       'Red' => 'Red',
  52.       'Yellow' => 'Yellow',
  53.     ),
  54.     '#default_value' => $default_color,
  55.     '#description' => "Please select your favorite color.",
  56.   );
  57.   return $form;
  58. }
  59.  
  60. /**
  61.  * Form builder for page 3
  62.  */
  63. function my_module_my_form_page_3($form_state) {
  64.   $default_food = $form_state['values']['food'];
  65.   // load the stored data
  66.   if (isset($form_state['storage']['page_values_3'])) {
  67.     $default_food = $form_state['storage']['page_values_3']['food'];
  68.   }
  69.   $form['title'] = array(
  70.     '#type' => 'markup',
  71.     '#value' => '<h3>Page 3</h3>',
  72.   );
  73.   $form['food'] = array(
  74.     '#type' => 'textfield',
  75.     '#title' => 'Favorite Food',
  76.     '#default_value' => $default_food,
  77.     '#description' => "Please enter your favorite food.",
  78.   );
  79.   return $form;
  80. }
  81.  
  82. /**
  83.  * Form builder for page 4
  84.  */
  85. function my_module_my_form_page_4($form_state) {
  86.   // get stored data
  87.   $name = $form_state['storage']['page_values_1']['name'];
  88.   $country = $form_state['storage']['page_values_1']['country'];
  89.   $color = $form_state['storage']['page_values_2']['color'];
  90.   $food = $form_state['storage']['page_values_3']['food'];
  91.  
  92.   // generate themed summary table (just make it look nicer)
  93.   $header = array(
  94.     array('data' => 'Title'),
  95.     array('data' => 'Entry'),
  96.   );
  97.   $rows[] = array(
  98.     array('data' => 'Name'),
  99.     array('data' => $name),
  100.   );
  101.   $rows[] = array(
  102.     array('data' => 'Country'),
  103.     array('data' => $country),
  104.   );
  105.   $rows[] = array(
  106.     array('data' => 'Favorite Color'),
  107.     array('data' => $color),
  108.   );
  109.   $rows[] = array(
  110.     array('data' => 'Favorite Food'),
  111.     array('data' => $food),
  112.   );
  113.   $summary = '<h3>Page 4 (Summary)</h3>';
  114.   $summary .= theme('table', $header, $rows);
  115.   $summary .= '<br/>';
  116.  
  117.   $form['summary'] = array(
  118.     '#type' => 'markup',
  119.     '#value' => $summary,
  120.   );
  121.   return $form;
  122. }
ここは各ページ独自のフォームを生成しているサブルーチンです。ソースを見やすくするために敢えてページごとに独立してサブルーチンにしました。
最終ページは設定項目はなく、確認画面としてそれまでのページで入力した項目をテーブルにして表示しています。

各ページのサブルーチンの先頭で、$form_state['storage']['page_values_X']というのが設定されているかをチェックし、設定されていればその配列から入力項目のデフォルト値を取得してます。これはこの後に出てくるsubmitハンドラでセットするのですが、入力された項目をページごとにまとめて$form_state['storage']['page_values_X'](Xはページ番号)に保管しておきます。

通常、こういったマルチフォームなどの処理はセッション変数を利用して一時的に保管するのですが、この記事の元になった記事でこういった$form_stateを利用した仕組みを使っているのでこのモジュールでもそれにならっています。

  1. /**
  2.  * Form validator
  3.  */
  4. function my_module_my_form_validate($form, &$form_state) {
  5.   // we do not validate the page when Back button is pressed
  6.   if ($form_state['clicked_button']['#id'] == 'edit-back') {
  7.     return;
  8.   }
  9.   $page = $form_state['storage']['page'];
  10.   switch ($page) {
  11.     case 1:
  12.       if (empty($form_state['values']['name'])) {
  13.         form_set_error('name', 'Please enter your name.');
  14.       }
  15.       if (empty($form_state['values']['country'])) {
  16.         form_set_error('country', 'Please enter your country.');
  17.       }
  18.       break;
  19.     case 2:
  20.       if (empty($form_state['values']['color'])) {
  21.         form_set_error('color', 'Please enter your favorite color.');
  22.       }
  23.       break;
  24.     case 3:
  25.       if (empty($form_state['values']['food'])) {
  26.         form_set_error('food', 'Please enter your favorite food.');
  27.       }
  28.       break;
  29.     case 4:
  30.       // nothing to validate
  31.       break;
  32.   }
  33. }
ここはフォームの移動時にフォームの検証をおこなう関数です。ここでも$form_state['storage']['page']から現在のページ番号を取得して、そのページごとにフォームの入力項目の検証をおこなっています。検証の結果エラーがある場合は、form_set_error関数をコールするだけであとはDrupalのフォームAPIが処理してくれます。
あと、この関数の先頭で押されたボタンが「Back」の場合には入力項目の検証はおこなわないようにしています。「Back」の時に検証してしまうと、「Next」で新しいページに来た直後に前のページに戻りたくなったときに、そのページの入力項目をちゃんと入力しないと前のページに戻れないのはよろしくないからです。

  1. /**
  2.  * Form submit handler
  3.  */
  4. function my_module_my_form_submit($form, &$form_state) {
  5.   $page = $form_state['storage']['page'];
  6.   if ($form_state['clicked_button']['#id'] == 'edit-back') {
  7.     $form_state['storage']['page']--;
  8.     // save the current page data
  9.     $form_state['storage']['page_values_' . $page] = $form_state['values'];
  10.   }
  11.   else if ($form_state['clicked_button']['#id'] == 'edit-next') {
  12.     $form_state['storage']['page']++;
  13.     // save the current page data
  14.     $form_state['storage']['page_values_' . $page] = $form_state['values'];
  15.   }
  16.   else { // submit button
  17.     //**********************************************************************
  18.     // We have finally done! Add code to do something - save data, etc.
  19.     //**********************************************************************
  20.     drupal_set_message('Your form has been submitted');
  21.     unset ($form_state['storage']);
  22.     $form_state['redirect'] = 'node'; // Redirects to the top page
  23.   }
  24. }
これはフォームのsubmitハンドラです。「Back」「Next」「Submit」ボタンが押された時にコールされます。ここでは前後のページの移動の場合には、$form_state['storage']['page']を+1もしくは-1しています。またそれと同時に、現在のページの入力項目を一括して$form_state['storage']['page_values_X'](Xはページ番号)に入れています。こうすることで他のページにいっても入力項目の情報は失われることなく残ります。ここも通常であればセッション変数でやるところでしょう。
「Submit」ボタンが押された場合には、このサンプルでは何もしないでサイトのトップページにジャンプして「Your form has been submitted」とメッセージを表示するようにしています。実際には、ここで入力項目のデータを元に処理をおこなったり、入力データをデータベースに保管したりといった処理をおこなうことになります。「Submit」されて処理がおわった時点で、もう不要となった$form_state['storage']をunsetして削除しています。

  1. /**
  2.  * Form reset handler
  3.  *
  4.  * Entire form data is cleared and go back to the page 1
  5.  */
  6. function my_module_my_form_clear($form, &$form_state) {
  7.   unset ($form_state['values']);  // ensures fields are blank after reset
  8.                                   // button is clicked
  9.   unset ($form_state['storage']); // ensures the reset button removes the
  10.                                   // new_name part
  11.   $form_state['rebuild'] = TRUE;
  12. }
  13. ?>
さいごは各ページにある「Reset form」ボタンを押した時に呼び出されるハンドラです。submitハンドラに処理を入れてもよかったのですがあえて見やすくするために別にしています。ここではフォーム全体のクリアをおこなっています。
$form_state['rebuild'] = TRUE;によりフォームAPIは同じページのフォームビルダーを再度コールします。これによりクリアされた内容で同じページのフォームが表示されることになります。このモジュールでは$form_state['storage']をクリアすると、$form_state['storage']['page']も当然なくなりますのでメニューからコールした時の同じ初期状態になり、ページ1のフォームが表示されることになります。

以上です。この記事が誰かの役に立てば幸いです。

ダウンロード

 

添付サイズ
my_module-6.x-1.x.tar.gz2.22 KB
あなたの評価: なし 平均: 4 (1 vote)