アイキャッチ画像: デスクの上でパソコンを使用している

今回は Drupal 7 でフォーム機能を開発する上で欠かせない $form_state 変数について取り上げてみたいと思います。 今回は少しマニアックな技術者の方向けの内容になります。

  /**
   * すごいフォームを生成する
   */
  function mymodule_build_gorgeous_form($form, &$form_state) {
    // フォームフィールドを定義
  }

$form_state とは

$form_state とは、フォームに関する一切の情報を格納した変数(連想配列)です。 $form 変数がフォームの「構築」に関する情報を格納するものであるのに対し、 $form_state はフォームのその他の情報をすべて格納したものです。

その中心的な役割は、ユーザがフォームを通じてサイトに送信した値を、一貫したフォーマットでプログラマに(ドメイン層のコードに)渡すことです。 ただしそれだけでなく、バリデーション、マルチステップフォーム、送信完了ページの表示やキャッシュのコントロールなどに欠かせないフォームにまつわる情報を一元的に管理しています。

個人的に、 $form_state に関するインターネット上の情報は英語でも充実したものがあまり無く、このあたりのことを調べるのはいつもなかなか苦労しています。 今回は私と同じように Drupal の Form API を活用したいと考えてい(ていつも苦労してい)る技術者の方を想定読者とし、 drupal_build_form() 関数のコメントやフォーム関連 tips をかんたんにまとめてご紹介してみたいと思います。

$form_state の要素について( drupal_build_form() のコメントから)

$form_state にはいろんな情報が格納されています。 drupal_build_form() 関数に添えられたコメントをもとにその代表的な情報をご紹介してみたいと思います。

基本的には原文直訳になりますが一部わかりやすいように意訳しています。 原文の厳密な意味合いが知りたい方は原文( Drupal 7 の includes/form.inc ファイル)の方にあたってみてください。

$form_state の代表的な要素:

  • build_info: 内部で使用。 Form API によって保持される情報の連想配列。もともとのコンテクストが利用できなくなったときにフォームをキャッシュからビルド/リビルドするときに必要です。
    • args: フォームコンストラクタに渡される配列。
    • files: フォームのビルドのために読み込みが必要な include ファイルを定義したオプション配列。各配列はファイルへのパスまたは module_load_include() に必要な 'type' や 'module' 、 'name' などの引数の値を表す配列です。ここにあげられたファイルは form_get_cache() で自動的に読み込まれます。デフォルトでは、現在のメニュールーターアイテムの 'file' の定義が(もしあれば)追加されます。 include ファイルを追加するにはフォームコンストラクタ内で form_load_include() を指定してください。
    • form_id: 構築あるいは処理されるフォームの ID 。
    • base_form_id: hook_forms() の中で宣言されたベースフォームの ID 。
    • immutable: このフラグが TRUE にセットされると、フォームがキャッシュから読み込まれるときにフォームビルド ID が新たに生成されます。後ほどキャッシュへの保存が再度行われる際には別のキャッシュ ID を持ちます。結果として、もともとのフォームと form_state は変更されずに残ります。これは、ページキャッシュが有効なときに匿名ユーザの間で form_state が漏れるのを防ぐために重要です。
  • rebuild_info: 内部で使用。 'build_info' と似ていますが、 drupal_rebuild_form() に対応しています。
  • rebuild: 通常、フォームの処理がすべて完了しサブミットハンドラが走ったら、フォームは完了したものとみなされ drupal_redirect_form() がユーザを GET リクエストで新しいページにリダイレクトします(だからブラウザリフレッシュではフォームの再送信は起こりません)。しかし、 'rebuild' が TRUE にセットされると、リダイレクトのかわりに即座にフォームの新しいコピーが作られてブラウザに送信されます。これは、ウィザードや確認フォームなどのマルチステップフォームで使用されます。フォームが完了したか別のステップが必要かの判断のロジックはふつうサブミットハンドラにあるべきものなので、通常この $form_state['rebuild'] はサブミットハンドラによってセットされます。しかし、バリデーションハンドラが form_state['rebuild'] をセットすることもでき、その場合にはフォーム処理はサブミットハンドラをスキップしてフォームを再構築することになります(バリデーションエラーがなくてもそうなります)。
  • redirect: フォームの送信時のリダイレクトに使用します。その内容は destination URL を含む文字列でも drupal_goto() に対応した引数の配列でも OK です。完全な情報は drupal_redirect_form() をご覧ください。
  • no_redirect: TRUE にセットされるとフォームは drupal_goto() を実行しません。これは 'redirect' がセットされている場合でもそうなります。
  • method: このフォームのユーザからの入力を見つけるために使用する HTTP フォームメソッド。値は 'post' または 'get' です。デフォルトは 'post' です。 'get' メソッドのフォームは常に送信とみなされ、フォーム ID を使用しません。データを変更する処理は 'post' のみのドメインなので、 'get' メソッドはデータの変更を行わないフォームでのみ使用しましょう。
  • cache: TRUE にセットされると、もともとの未処理のフォーム構造がキャッシュされます。結果、フォーム全体がキャッシュから再構築されることになります。典型的なフォームのワークフローは 2 回のページリクエストを必要とします。 1 回目に、フォームが構築されユーザーの記入のために描画されます。続いて、ユーザがフォームに記入し送信します。そこで 2 回目のページリクエストが発生し、フォームが構築、処理されます。デフォルトではこれらの各ページリクエストで $form$form_state はゼロから構築されます。しばしば変数 $form$form_state を最初のページリクエストからサブミット処理を行うリクエストに引き継ぐ必要性や要求が出てきて、その場合に 'cache' を TRUE にセットすることができます。よい例は Ajax が使用されたフォームです。 ajax_process_form()#ajax プロパティを持つ要素を含んだすべてのフォームでキャッシュを有効化します( Ajax ハンドラはフォームそのものを構築する方法を持たないため、キャッシュされたフォームに依存しなくてはなりません)。 $form$form_state の引き継ぎは 'rebuild' フラグがセットされた(マルチステップの)フォームで 'cache' の値によらず自動的に行われることに注意してください。
  • no_cache: TRUE にセットされると、フォームはキャッシュされません。 'cache' がセットされた場合でもキャッシュは発生しません。
  • values: フォームに送信された値の連想配列。バリデーション関数とサブミット関数はほぼすべての判断においてこの配列を使用します(値がフラットな配列か $form と同じ構造になるかは #tree が決定することに注意してください。詳しくは forms_api_reference.html Form API reference をご覧ください)。これらはバリデーションが行われていない生の値なので、セキュリティ上のポイントをしっかり理解できないままの状態で使うべきではありません。ほぼすべての場合において、コードは 'values' 配列のデータのみを使用するようにすべきです。このキーのもっとも一般的な使い方は、 'rebuild' をセットするときにユーザ入力の一部をクリアする必要があるマルチステップフォームで使う場合です。 この値は選択された method に応じて $_POST または $_GET に対応します。
  • always_process: この値が TRUE にセットされていてメソッドが GET の場合、 form_id は必要ありません。これはセキュリティイシューにつながるので、データ書き込みのない RESTful GET フォームでのみ使用しましょう。これは、検索をかける際にクエリパラメータに form_id を入れなくてもいいようにするのに役立ちます。
  • must_validate: フォームは通常一度だけバリデートされますが、フォームが内部的に再送信されバリデーションを再度行うべき場合があります。これを TRUE にするとその動作を強制することができます。このようなケースは Ajax 操作中に発生することが多いでしょう。
  • programmed: TRUE ならフォームはプログラムによって送信されました。通王は drupal_form_submit() で実行されています。デフォルトは FALSE です。
  • programmed_bypass_access_check: TRUE ならプログラムでのフォーム送信は #access を考慮せずに処理されます。現在のリクエストを実行しているユーザの入力値を使ってプログラムでフォームを送信するときにはこれを TRUE にしてください。すると、通常のフォーム送信であるかのように #access が取り扱われます。デフォルトは TRUE です。
  • process_input: 真偽値のフラグ。この値が TRUE だと正しいフォーム送信であることを意味します。 drupal_form_submit() から来たプログラムによるフォームに対してこれは常に TRUE となります( 'programmed' キーを参照のこと)。また、 $_POST データで form_id がセットされてそれが現在の form_id に一致する場合に TRUE となります。
  • submitted: TRUE ならフォームは送信済みです。デフォルトは FALSE です。
  • executed: TRUE ならフォームは送信済みでかつ処理、実行済みです。デフォルトは FALSE です。
  • triggering_element: (読み取り専用)送信をトリガーしたフォーム要素。これは廃止の $form_state['clicked_button'] と同じです。これは送信を引き起こした要素であり、ボタンとはかぎりません( Ajax の場合など)。このキーはサブミットハンドラが複数のボタンのうちどのボタンが使用されたかを区別するためにしばしば利用されます。 Ajax ハンドラでも使用されます。
  • clicked_button: 廃止予定。代わりに triggering_element を使いましょう。
  • has_file_element: 内部で使用。 TRUE ならファイル要素が存在します。 Form API は適切な HTML の 'enctype' 属性をフォームにセットします。
  • groups: 内部で使用。垂直方向のタブで画面に描画するためにフィールドセットへのリファレンスを持つ配列。
  • storage: $form_state['storage'] は特別なキーではなく、 Form API でそのためのサポートが提供されているわけではありません。これは伝統的にアプリケーション固有のデータが保存される場所となっており、(特にマルチステップタイプのフォームで)サブミット、バリデーション、フォームビルダー関数の間でのコミュニケーションをサポートするために使用されます。フォームを実装するときは、この類の保存領域として $form_state 内のどのキーでも(ただしここにリストアップされているキーや Form API が内部的に使用する予約されたもの以外)使用することができます。選んだキーが Form API や他のモジュールが使用するものと衝突しないようにするおすすめの方法は、モジュール名をキーの名前やキーのプレフィックスとして使用するというものです。例えば、 Node モジュールはノード編集フォームにおいて編集中のノード情報を格納するために $form_state['node'] を使用します。この情報は「プレビュー」ボタンがクリックされその後最終的に「保存」ボタンがクリックされるまでの間利用することができます。
  • buttons: フォーム内のすべての submit 要素と button 要素のコピーを格納したリスト。
  • complete form: 完全なフォーム構造を格納した $form 変数へのリファレンス。 #process#after_build#element_validate やフォーム要素に対して実行されるその他のハンドラはこのリファレンスを使用して、その要素が含まれるフォームの他の要素にアクセスすることができます。
  • temporary: 現在のページリクエストの間だけアクセスできる一時データを格納する配列。予約されたものを除く $form_state のプロパティ( form_state_keys_no_cache() をご覧ください)はすべてマルチステップフォームの流れの中で維持されます。ひとつのページリクエストの中でフォーム関連の機能の間での情報をやりとりをモジュールができるようにするため、 Form API はこのキーを提供しています。これは、フォームのワークフローの全体でキャッシュされる必要のない、あるいはキャッシュされるべきでないデータを一時的に保存するために使用することができます。一例は、現在のフォームビルドの処理の中でだけアクセスできるべきデータなどです。この機能については Drupal コア内にユースケースはありません。
  • wrapper_callback: マルチステップフォームウィザードにおける戻る/進む/保存ボタンなどのような共通の要素をフォームに追加したいモジュールでは、フォーム構造を返すフォームビルダー関数の名前を定義することができます。するとそのフォーム要素は実際のフォームビルダー関数に渡されます。この実装の際には、 hook_forms() を使って 'wrapper_callback' を定義するか、そうでなければカスタムメニューコールバックで drupal_build_form() を( drupal_get_form() ではありません)自身で呼び出して $form_state を準備する必要があります。

以上です。

$form_state 関連 tips

$form_state にまつわるもろもろの tips をちょこっとだけですがご紹介してみたいと思います。

1. フォームに送信された値を使用したい

ユーザがフォームを通じてサーバに送信した値を利用したい――これはフォームを利用する上での基本中の基本ですね。

フォームに送信された値を取得するには $form_state['values'] に格納された配列を利用すれば OK です。 利用イメージは PHP でフレームワークを使わない場合に利用する $_POST と同じイメージです。

/**
 * 独自のコメントフォームを構築する
 */
function mymodule_build_comment_form($form, &$form_state) {

  // 名前フィールドを追加
  $form['name'] = array(
    '#type' => 'textfield',
    '#title' => '名前',
  );

  // コメントフィールドを追加
  $form['comment'] = array(
    '#type' => 'textarea',
    '#title' => 'コメント',
  );

  return $form;
}

/**
 * コメントフォームの送信処理を行う
 */
function mymodule_build_comment_form_submit(&$form, &$form_state) {

  // 入力された名前とコメントを取得
  $name = $form_state['values']['name'];
  $comment = $form_state['values']['comment'];

  // 送信処理を行う
  // ...
}

(このコードはあくまでも例です)

2. フォームが無事に処理されたら別の画面にリダイレクトしたい

つづいて、フォームの送信処理が無事に終わった後に別の画面にリダイレクトする方法についてです。

上の $form_state の各キーの説明のところですでに十分に説明されていますが、こちらは $form_state['redirect'] を利用すれば OK です。

/**
 * コメントフォームの送信処理を行う
 */
function mymodule_build_comment_form_submit(&$form, &$form_state) {

  // 入力された名前とコメントを取得
  $name = $form_state['values']['name'];
  $comment = $form_state['values']['comment'];

  // コメントエンティティを作成
  // $new_comment = ...

  // コメントエンティティの保存が無事成功したら投稿完了画面に遷移する
  $result = $new_comment->save();
  if ($result) {
    $form_state['redirect'] = 'url-for-thank-you-page';
  }
}

ここで 'url-for-thank-you-page' には通常 Drupal のベースルート以下のパス( comment/thank-you など?)を指定します。

Drupal 7 でリダイレクトといえば drupal_goto() です。 フォームの場合もこれを使ってリダイレクトすることもできますが、 $form_state['redirect'] の方は条件なども見て適切にリダイレクト処理をしてくれるので、フォームの場合は $form_state['redirect'] を使うのがよいお作法です。

3. フォームを .module 以外のファイルで定義するとどうも正しく動作しない

次に、 .module 以外のファイルでフォーム関連関数を宣言した場合の問題についてです。

カスタムモジュールのサイズが大きくなってきたときには、ほどよいサイズにファイルを分割するのがおそらく正解です。 ただ、この場合にきちんと対策をしておかないと Ajax サブミットハンドラなどが正しく動作してくれないことがあります。

そんなときには $form_state['build_info']['files'] をコントロールすれば OK です。 上であげたように、ここには「フォームの処理に必要な inc ファイルのパス」を格納することになっています。 ここにファイルを追加しておけば適当なタイミングで Drupal が対象ファイルを読み込んでくれます。

ただし $form_state['build_info']['files'] は直接触るものとして設計されていないので代わりに専用の関数 module_load_include() を使うようにしましょう。

/**
 * 独自のコメントフォームを構築する
 */
function mymodule_build_comment_form($form, &$form_state) {

  // フォームのライフサイクル中に必要になる inc ファイルを読み込み対象に追加
  form_load_include($form_state, 'inc', 'mymodule', 'comment-form');

  // フィールドを追加
  // ...
}

4. フォームが開かれたときの値を保持したい

フォームが開かれたときの値を保持したい場合についてです。

デフォルトでは Drupal のフォームビルダ関数はフォームの構築時と送信時にそれぞれ呼ばれることになっており、そのまま値をセットするだけでは「フォームが開かれたときの値」をサブミットハンドラで利用することができません。

フォームの送信時ではなく最初の構築時の値を取得して保持しておきたいときは $form_state['cache'] を利用すれば OK です。 ちなみにユーザに見せずに内部的にだけ持っておきたい値は $form_state['storage'] に保持するのがデファクトスタンダードです(ただ、コアのコードを見ると必ずしもそうでもないようです・・・)。

/**
 * 独自のコメントフォームを構築する
 */
function mymodule_build_comment_form($form, &$form_state) {

  // フォーム構築時の時刻を保持しておく
  $form_state['storage']['initial_request_time'] = REQUEST_TIME;
  $form_state['cache'] = TRUE;

  // フィールドを追加
  // ...
}

/**
 * コメントフォームのバリデーション処理を行う
 */
function mymodule_build_comment_form_validate(&$form, &$form_state) {

  // フォーム構築時の時刻を取得
  $initial_request_time = $form_state['storage']['initial_request_time'];

  // 例: フォームが 3 秒以内に送信されなければアウト
  if (!mymodule_is_in_3seconds(REQUEST_TIME, $initial_request_time)) {
    form_set_error(NULL, 'もっと速く送信してください。');
  }
}

このあたりは $form_state['cache']$form_state['storage'] の説明をじっくり読めばわかりますが、なかなかわかりづらいですよね。。

5. フォーム送信をトリガーしたボタンを特定したい

つづいて、フォームがどのボタンを押して送信されたかをチェックしたい場合です。

ひとつのフォームに複数のボタン、あるいはボタン以外でも送信処理をトリガするような要素が存在する場合はバリデーションハンドラやサブミットハンドラの中で $form_state['triggering_element'] を参照すれば OK です。

6. フォームフィールドに JavaScript のエフェクトや Ajax 処理をくっつけたい

チェックボックスにチェックが入ったらテキストエリアを表示する、入力された値の妥当性を Ajax でサーバ側に問い合わせる、といった処理をしたい場合。

これには $form_state ではなく、 $form 変数を使用します。

フォームの表示/非表示などの状態を条件によって切り替えたい場合は #state キーを、 Ajax 処理を付与したい場合は #ajax キーをそれぞれ使えば OK です。

このあたりは少し長くなり、 $form_state の話題から外れるので詳細の説明はまたの機会に譲りたいと思います。

・・・

以上です。 いかがだったでしょうか?

今回は Drupal 7 のフォームを使った開発に欠かせない $form_state 変数について取り上げてみました。 このあたりは PHP ならではの使いづらさというか難しさはありますが、そこには目をつむれば「よく考えられているなぁ」と思うところがたくさんあります。 よく知ってうまく活用できれば Drupal のフォーム開発がよりスピーディになるかと思いますので、「 $form_state のあれってどうなっているんだっけ?」となったときのご参考にしていただければと思います。