(雪だるまの画像は pixabay からいただきました。帰属表記は必要ないとのことですがリンクを。)

本日は Drupal Advent Calendar 2015 の 22 日目の記事ということで「 Drupal 8 の Form API 」をテーマに記事を書いてみたいと思います。

Drupal 8 のフォームを実際に作成しながらその新しい Form API について見ていきます。 まず最初にフォーム作成の一連の流れをざっくりと追ってから、その後に解説として Drupal 7 ウェイとのちがいなどを見ていければと思います。

以下本記事の目次です。

  • はじめに
  • Drupal 8 でフォームを作ろう
  • Ajax 機能を追加しよう
  • 解説
  • おわりに

次は完成形のフォームのイメージです。

フォーム完成形

はじめに

実際にフォームを作っていく前に Drupal 8 をセットアップしましょう。 Drupal 8 のセットアップそのものは Drupal 7 に馴染みのある方であれば同様の手順でささっと問題なく進められるかと思います。

Drupal 7 に馴染みのない方は「 Drupal 8 インストール」「 Drupal インストール」などで検索してみてください。 今回は Form API に絞ってご紹介したいので、セットアップについてはそちらをご参照いただければと思います。

Drupal 7 から 8 に上がるにあたり PHP やデータベースの必須バージョンも上がっていますので、そのあたりにはぜひご注意ください。

Drupal 8 必須バージョン:

  • PHP: 5.5.9 以上
  • データベース: MySQL 5.5.3 / MariaDB 5.5.20 / Percona Server 5.5.8 など。 PostgreSQL 9.1.2 以上。 SQLite 3.6.8 以上。

本記事のテーマ外となりますが、最近よく目にする Drupal console を使うと Drupal 8 の開発はスムーズです。 Drupal console についてもここで触れていきたいところではありますがここで寄り道するとキリがなくなりそうなので(笑)今回は Form API に的を絞ることにします。 Drupal console に興味のある方は以下のページやスライドをご覧になってみてください。

Drupal 8 でフォームを作ろう

Drupal 8 サイトがセットアップできて準備が整ったら実際にフォームを作っていきましょう。 今回は「 JR 琵琶湖線の駅名を調べる機能」を提供するかんたんなフォームを作ってみます。

Drupal でのコードを使ったフォーム作成の一般的な手順は以下のとおりです。

  1. モジュールの作成
  2. コントローラの作成
  3. フォームの作成
  4. フォームのカスタマイズ

以下順番に見ていきましょう。

1. モジュールの作成

まずはフォームの入れ物となるモジュールを用意します。 今回は「 Biwako 」という名前のモジュールを作りましょう。

Drupal ルートの modules 以下に biwako という名前のディレクトリを作成し、その中に以下の 2 つのファイルを作成しましょう。

modules/biwako/biwako.info.yml:

name: Biwako
type: module
description: Provides a page which returns Biwako-sen stations.
core: 8.x
package: umi

modules/biwako/biwako.module:

<?php

.yml の方は YAML ファイル、 .module の方は PHP ファイルとしてシンタックスハイライトするようにエディタに設定しておくと読みやすくなります。 biwako.module の中身については <?php だけとなっていますが今のところはこれで問題ありません。 空にしたままの状態で次へと進んでいきましょう。

ファイルが問題なく作れたら Drupal が自動的にこれをモジュールとして認識してくれるようになります。 管理画面の「機能拡張」のページ(パス: /admin/modules )に Biwako モジュールが追加されているので、そちらを確認し有効化しましょう。 今のところコードが入っていない空のモジュールなので特に機能などは追加されません。

次はこのモジュールが提供するページを作成していきます。 後ほどこのページにフォームを入れていくことになります。

2. コントローラの作成

コントローラを作成します。 コントローラとは MVC モデルでいうところの C の部分です。 コントローラだけ作っても仕方がないので、対応するルーティングの設定もあわせてここで行います。

Drupal 7 では手続き型スタイルがベースになっておりコントローラに該当する page callback は関数となっていましたが、 Drupal 8 ではコントローラクラスのメソッドを page callback とする主流の WAF のスタイル(?)が採用されています。

コントローラについては以下のファイルを作成すれば OK です。

modules/biwako/src/Controller/BiwakosenController.php:

<?php

/**
 * @file
 * Contains \Drupal\biwako\Controller\BiwakosenController.
 */

namespace Drupal\biwako\Controller;

use Drupal\Core\Controller\ControllerBase;

/**
 * Class BiwakosenController.
 *
 * @package Drupal\biwako\Controller
 */
class BiwakosenController extends ControllerBase {
  /**
   * Index.
   *
   * @return array
   *   琵琶湖線駅名調べフォーム。
   */
  public function index() {
    $form = \Drupal::formBuilder()->getForm('Drupal\biwako\Form\BiwakosenForm');
    return $form;
  }
}

このコントローラクラスを格納したファイルの場所や名前については PSR-4 に従う必要があるためご注意ください。 モジュールのルートに適当に入れても正しく認識してくれません。

つづいて対応するルーティング設定を行います。 ルーティング設定には YAML ファイルを利用します。

以下のファイルを追加しましょう。

modules/biwako/biwako.routing.yml:

biwako.biwakosen_controller_index:
  path: '/biwako/biwakosen'
  defaults:
    _controller: '\Drupal\biwako\Controller\BiwakosenController::index'
    _title: '琵琶湖線駅名調べ'
  requirements:
    _permission: 'access content'

ここでも詳細は割愛しますが、 Drupal 8 ではルーティングの仕組みが大幅に見直されこれまで hook_menu() で一元化されていたルーティング、メニュー、タブなどの設定を別個に定義/管理する形になりました。

ひとまずここでは、ルーティング設定として以下のことを行っていることのみ押さえておいていただければと思います。

  • /biwako/biwakosen というパスへのアクセスがあったときに
  • リクエストの処理を \Drupal\biwako\Controller\BiwakosenController というクラスの index というメソッドに任せる。
  • ページタイトルは「琵琶湖線駅名調べ」とする。
  • ページは access content という権限を持ったユーザが見れるように設定する。

また、上の BiwakosenController コントローラの index メソッドの方では以下のことを行っているのでこちらも念頭に入れておいてください。

  • Drupal\biwako\Form\BiwakosenForm というクラスが定義したフォームを表示する。

BiwakosenForm クラスは次のステップで作成するつもりなのでこの時点では未実装ですが、この時点でモジュールが定義したページが処理できるようになっているはずです。 キャッシュをリビルドしてから /biwako/biwakosen にアクセスしてみましょう。 以下のようなメッセージが表示されるのではないかと思います。

サイトに予期せぬエラーが起こりました。しばらくたってから再度お試しください。

このエラーの原因は呼び出すべきフォーム BiwakosenForm が存在しないことです。 フォームは次のステップで作るので、ここでは Not found ではなくこのエラーが返ってくるようになるのが正解です。

コントローラが正しく動くことをこの時点でもう少し細かく確認したければ(後のステップでデバッグする場合も) index メソッドの中身の先頭に以下のような return 文を追加してみてください。 /biwako/biwakosen にアクセスしたときにページが表示されメインコンテンツの本文内に Hello, Biwako! と表示されるようになるはずです。

  public function index() {
    return array(
      '#markup' => 'Hello, Biwako!',
    );
    // 以下オリジナルのコード...
  }

詳細は後述します。

以上でコントローラとルーティングの設定は完了です。 次のステップからいよいよフォーム中身を作成していきます。

3. フォームの作成

フォームを作成しましょう。

Drupal 7 でフォームを作成するには、規約に沿った形で「フォームの作成」と「バリデーション」、「送信後の処理」を行う 3 つの関数をセットで定義する形になっていました。 Drupal 8 ではオブジェクト指向ベースとなり、規定されたインタフェース FormInterface を実装する形でフォームを作成することになります。

以下のファイルを作成しましょう。

modules/biwako/src/Form/BiwakosenForm.php:

<?php

/**
 * @file
 * Contains \Drupal\biwako\Form\BiwakosenForm.
 */

namespace Drupal\biwako\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Class BiwakosenForm.
 *
 * @package Drupal\biwako\Form
 */
class BiwakosenForm extends FormBase {

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'biwakosen_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $form['number_wrapper'] = array(
     '#type' => 'details',
     '#title' => '京都駅から◯番目にある琵琶湖線の駅名を調べます。',
     '#collapsible' => TRUE,
     '#collapsed' => FALSE,
    );

    $form['number_wrapper']['number'] = array(
      '#title' => '京都駅からの駅数',
      '#type' => 'number',
      '#step' => 1,
      '#default_value' => 0,
      '#required' => TRUE,
    );

    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => '調べる',
    );

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    $number = $form_state->getValue('number');
    $stations_count = count($this->getStations());

    if ($number < 0) {
      $message = $this->t('負の数はいけません。', []);
      $form_state->setErrorByName('number', $message);
    }
    else if ($number > $stations_count - 1) {
      $message = $this->t('京都駅から @n 駅離れたらそこはもう琵琶湖線ではありません。', ['@n' => $number]);
      $form_state->setErrorByName('number', $message);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $number = $form_state->getValue('number');
    $stations = $this->getStations();
    $station = $stations[$number];

    $message = new FormattableMarkup('京都駅から @n 駅目にある駅、それは・・・ @station です。', [
      '@n' => $number,
      '@station' => $station,
    ]);
    drupal_set_message($message);
  }

  /**
   * 琵琶湖線の駅一覧を返す
   */
  private function getStations() {
    return array(
      '京都駅',
      '山科駅',
      '大津駅',
      '膳所駅',
      '石山駅',
      '瀬田駅',
      '南草津駅',
      '草津駅',
      '栗東駅',
      '守山駅',
      '野洲駅',
      '篠原駅',
      '近江八幡駅',
      '安土駅',
      '能登川駅',
      '稲枝駅',
      '河瀬駅',
      '南彦根駅',
      '彦根駅',
      '米原駅',
      '坂田駅',
      '田村駅',
      '長浜駅',
    );
  }
}

細かな部分は後述しますが、ここではひとまず以下のことをやっているということを押さえていただければと思います。

  • 琵琶湖線の駅名を返すフォームを作成する。具体的には以下のとおり。
  • HTML5 の number フィールドで数値を受け付けるフィールドを作成する。
  • フォーム送信時のバリデーションとして、想定している数字以外の数字が渡されたらバリデーションエラーを戻す。
  • フォーム送信成功時の処理として、琵琶湖線の駅名を返す。

フォームクラスが完成したら、先ほどのページでフォームが表示できるようになっているはずです。 キャッシュをリビルドしてから /biwako/biwakosen にアクセスしてみましょう。 フォームは表示されましたか?

これにて Drupal 8 でのフォーム作成の一連のステップは完了です。 あとは実際の要件に従って細かく作りこみをしていけばよいでしょう。

Drupal のフォームといえば form alter !ということで、もうひとつフォームのカスタマイズの方法についても押さえておきます。

4. フォームのカスタマイズ

既存のフォームのカスタマイズ、いわゆる form alter の方法は Drupal 7 と同じです。 該当するフォームの Form ID を使ってフォーム変更用のフックを書きましょう。

すでに作成した biwako.module ファイルに以下のコードを追加してください。 こちらは PHP コードである必要があるので <?php よりも後に書くようにしましょう。

modules/biwako/biwako.module:

/**
 * Implements hook_form_biwakosen_form_alter().
 */
function biwako_form_biwakosen_form_alter(&$form, FormStateInterface &$form_state) {
  // 説明がないとわかりづらいので説明文を追加
  $form['number_wrapper']['number']['#description'] = '正の整数を入力してください。';
}

これは Drupal 7 と共通の hook_form_FORM_ID_alter() です。 Drupal 8 ではフォームビルダ関数がメソッドになったため FORM_ID は関数ではなくフォームクラスが実装した getFormId() メソッドの戻り値を設定します。 今回上のフォームクラスでは getFormId() の戻り値を biwakosen_form としているのでここでのフック関数名は biwako_form_biwakosen_form_alter となります。

ここでは次のことをしているということを押さえていただければと思います。

  • BiwakosenForm クラスが描画するフォームを別の場所から変更して説明文言「正の整数を入力してください。」を追加する。

再びキャッシュをリビルドしてページを確認すると、数値フィールドの下に「正の整数を入力してください。」という文言が追加されていることが確認できるのではないでしょうか。

このあたりも詳細の解説は後述します。

もうひとつ、 Drupal のフォームの目玉機能のひとつ Ajax をちらっとだけ見てみましょう。

Ajax 機能を追加しよう

フォームの一連の処理がひととおり作れたので、発展編として Ajax 機能を追加してみましょう。 Drupal には Ajax API という名前そのままの Ajax 処理の API がコアにデフォルトで備わっており、 JavaScript コードを一行も書かずに定番の Ajax 処理を実装できるようになっています。

今回はこちらの Form API を使って上で作ったフォームに Ajax 機能を追加してみましょう。 説明は後述するとして BiwakosenForm.php の内容を以下のとおりに書き換えます。 ごっそり書き換えてしまって大丈夫です。

modules/biwako/src/Form/BiwakosenForm.php:

<?php

/**
 * @file
 * Contains \Drupal\biwako\Form\BiwakosenForm.
 */

namespace Drupal\biwako\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;

/**
 * Class BiwakosenForm.
 *
 * @package Drupal\biwako\Form
 */
class BiwakosenForm extends FormBase {

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'biwakosen_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $form['number_wrapper'] = array(
     '#type' => 'details',
     '#title' => '京都駅から◯番目にある琵琶湖線の駅名を調べます。',
     '#collapsible' => TRUE,
     '#collapsed' => FALSE,
    );

    $form['number_wrapper']['number'] = array(
      '#title' => '京都駅からの駅数',
      '#type' => 'number',
      '#step' => 1,
      '#default_value' => 0,
      '#required' => TRUE,
      '#ajax' => array(
        'callback' => array($this, 'submitFormAjax'),
        'event' => 'change',
        'progress' => array(
          'type' => 'throbber',
          'message' => '計算中',
        ),
      ),
    );

    $form['result'] = array(
      '#type' => 'markup',
      '#prefix' => '<div id="biwako-biwakosen-result">',
      '#suffix' => '</div>',
    );

    $form['counter'] = array(
      '#type' => 'markup',
      '#prefix' => '<div id="biwako-biwakosen-counter">',
      '#suffix' => '</div>',
    );

    $storage = $form_state->getStorage();
    if (!isset($storage['count'])) {
      $storage['count'] = 0;
    }
    else {
      $storage['count'] += 1;
    }
    $form_state->setStorage($storage);

    return $form;
  }

  /**
   * BiwakosenForm の Ajax コールバック
   */
  public function submitFormAjax(array &$form, FormStateInterface $form_state) {

    $number = $form_state->getValue('number');
    $stations = $this->GetStations();
    $station = $stations[$number];

    $message = $this->t('京都駅から @n 駅目にある駅、それは・・・ @station です。', [
      '@n' => $number,
      '@station' => $station,
    ]);

    $result = array(
      '#type' => 'markup',
      '#markup' => $message,
      '#prefix' => '<div id="biwako-biwakosen-result">',
      '#suffix' => '</div>',
    );

    $storage = $form_state->getStorage();

    $counter = array(
      '#type' => 'markup',
      '#markup' => 'カウンタ: ' . $storage['count'],
      '#prefix' => '<div id="biwako-biwakosen-counter">',
      '#suffix' => '</div>',
    );

    $response = new AjaxResponse();
    $response->addCommand(new ReplaceCommand('#biwako-biwakosen-result', $result));
    $response->addCommand(new ReplaceCommand('#biwako-biwakosen-counter', $counter));

    return $response;
  }

  /**
   * 琵琶湖線の駅一覧を返す
   */
  private function GetStations() {
    return array(
      '京都駅',
      '山科駅',
      '大津駅',
      '膳所駅',
      '石山駅',
      '瀬田駅',
      '南草津駅',
      '草津駅',
      '栗東駅',
      '守山駅',
      '野洲駅',
      '篠原駅',
      '近江八幡駅',
      '安土駅',
      '能登川駅',
      '稲枝駅',
      '河瀬駅',
      '南彦根駅',
      '彦根駅',
      '米原駅',
      '坂田駅',
      '田村駅',
      '長浜駅',
    );
  }
}

フォームを Ajax 処理する形に変更しました。 例をシンプルにするため submit ボタンと validateForm submitForm のふたつのメソッドは今回あえて削除しています。

/biwako/biwakosen にアクセスし、フォームの挙動が変わっていることを確認してみてください。 数値フィールドの値を(キーボードの上下カーソルキーなどを使って)変更すると、 Ajax によってページ全体のリロードなしに答えがすかさずページ内に表示されることがご確認いただけるのではないでしょうか。

今回は単発での値の処理の他に値を保持できるところを見るために(特に役割を果たさない)カウンタもつけてみました。 フォームの最下部で Ajax リクエストが走った回数がここでご確認いただけるとものと思います。

Drupal 8 の Ajax API を利用したフォーム

以上で Drupal 8 でのモジュールの作成、コントローラの作成から始まったフォームの作成、 Ajax 化のステップは終了です。 つづいて上から順に解説をしていきたいと思います。

解説

1. モジュールの作成

モジュールの作成ステップでは biwako という名前のモジュールを作成しました。 モジュール作成で具体的に作るべきは .info.yml ファイルと .module ファイルのペアです。

  • modules/biwako/biwako.info.yml:
  • modules/biwako/biwako.module:

Drupal 7 までカスタムモジュールを配置するディレクトリは sites/all/modules sites/example.com/modules などでしたが、 Drupal 8 では Drupal ルートの modules ディレクトリを使う形に変わっています。 一方でコアのモジュールは core/modules 以下にまとめて置かれる形になりました。

以下興味がある方はよろしければ。

上述の Drupal console を使うとこのあたりのモジュールテンプレートはターミナルから対話形式で作成することができます。 こちらだと間違いがない & 他のフレームワークとも似た形でとっつきやすいので開発現場ではこちらが主流になっていくように思います。

$ drupal generate:module

.info.yml ファイルの中身はシンプルなのであまり迷うべきところはありませんが、まずは以下の要素が必須とされていることについて押さえておけばよいでしょう。

  • name
  • type
  • description
  • core

じっくり見ていくと Drupal 7 の info ファイルに比べてパワーアップしている部分もちょこちょこ見られるので、興味のある方は以下の記事などをご覧になってみてください。

2. コントローラの作成

コントローラとルーティング設定の作成は Drupal 7 でいうところの page callback と hook_menu() へのアイテムの追加に相当します。

ルーティングの設定も .info.yml と同様に YAML 形式で行います。

biwako.biwakosen_controller_index:
  path: '/biwako/biwakosen'
  defaults:
    _controller: '\Drupal\biwako\Controller\BiwakosenController::index'
    _title: '琵琶湖線駅名調べ'
  requirements:
    _permission: 'access content'

ベースは Symfony のルーティング設定ですが、 Drupal ならではの部分がところどころ追加されているようです。 今回のように特定のルートとコントローラをひもづけて使う形がベースですが、自作のコントローラを介さずに(表面上)直接フォームビルダに処理を任せることも可能です。 フォームを直接呼び出す形に変更したい場合には _controller の部分を _form に変更すれば OK です。 今回のケースだと以下のように指定するとよいでしょう。

_form: '\Drupal\biwako\Form\BiwakosenForm'

ルーティング周りで加えた変更を反映するにはキャッシュのリビルドが必要なのでご注意ください。

$ drush cache-rebuild

URL パスから変数を取得したりなどルーティング周りもう少し凝ったことをしたい場合には以下の記事などがご参考になるかと思います。よろしければ。

今回作ったコントローラ BiwakosenController はただフォームを呼び出して描画しているだけですが、コントローラクラスのインスタンスにはさまざまな機能が備わっています。 コントローラ周りのインタフェースについては API のリファレンスページなどが参考になります。

ちなみに Drupal 7 では page callback が文字列を返す形も許容されていましたが、 Drupal 8 では文字列のリターンは NG になりました。 Drupal 8 で Drupal ウェイでレスポンスを返す場合、コントローラは原則レンダーアレイを返す必要があります。

3. フォームの作成

上述のとおり Drupal 8 のフォームはフォームクラスを使う形に変わりました。

Composer のクラスオートローダを活用するために、フォームクラスは PSR-4 に準拠してモジュールディレクトリ以下の src/Form/フォームクラス名.php に配置する必要があります。 ファイルの中には名前空間の宣言、利用するクラスを書いた後にクラスの中身を書きます。 フォームクラスは Drupal\Core\Form\FormBase を継承したクラスを定義し、以下の 4 つのメソッドを定義すれば OK です。

  • getFormId()
  • buildForm()
  • validateForm()
  • submitForm()

それぞれのメソッドの役割は次のとおりです。

  • getFormId(): フォームの ID を表す文字列を返す。この ID が他のモジュールが hook_form_FORM_ID_alter() を書いてフックをかけるときの FORM_ID となるため、サイト内でユニークな名称にする必要があります。
  • buildForm(): フォームの生成を担当する。 Drupal 7 でいうところのフォームビルダ関数。第 1 引数は Drupal 7 と同じ連想配列の $form ですが第 2 引数の $form_state は連想の配列から FormStateInterface を実装したクラスのインスタンスへと変わりました。実装したクラスのインスタンスへと変わりました。 Drupal 7 とほぼ同様の API ですがいくつか拡張が行われています。
    • HTML5 のフィールドタイプが追加: 'tel' 'email' 'number' 'date' 'url' 'search' 'range' など。
    • その他のフィールドタイプが追加: 'details''language_select' など。
  • validateForm(): フォームのバリデーションを担当する。 Drupal 7 でいうところの **_validate() 関数。フォームが送信されたときの値のチェックを行います。 $form_state がオブジェクトに変わったので、値の取得方法は $form_state->getValues()$form_state->getValue('name') に変わっています。 form_set_error() に代わる形で setErrorByName() メソッドが導入されました。 OOP スタイルになってわかりやすくなりましたね。
  • submitForm(): フォームのサブミット処理を担当する。 Drupal 7 でいうところの **_subit() 関数。通常はバリデーションが通った後にデータを永続化するような処理をここに書き込みます。今回は Drupal 7 と同様の drupal_set_message() 関数を使ってメッセージを表示する処理をしています。

また、今回作ったフォームクラスの部分では他にもいくつかポイントがあります。

  • Drupal 7 の t() 関数の代わりに t() メソッドを使っています。ソースまで終えていませんが、こちらは FormBase に trait として取り込まれているために使えるようになっているようです。
  • Drupal 7 の format_string() 関数の処理をするために FormattableMarkup クラスを使用しました。 t() メソッドに比べてこちらはちょっとひと手間かかるような感じです。調べられていませんがもっとよい呼び出し方があるように思います。
  • Drupal 7 で規約として使われていた $form_statestorage 要素が Drupal 8 ではクラスが正式に提供するものになりました。 $storage = $form_state->getStorage(); でストレージ全体の取得を $form_state->setStorage($storage); でストレージ全体の保存を行うことができます。

個人的には Drupal 8 で導入された名前空間のおかげでモジュールプレフィックスの必要がなくなり関数やクラスによりシンプルで直感的な名前を付けられるようになった点はとても大きなポイントだと思います。

フォーム周りについては 1 年以上前に少し書いたことがありますが、クラスファイルの配置場所のルールが PSR-0 準拠から PSR-4 に変わり FormStateInterface が導入されたという変化はありますが基本となる考え方などは大幅には変わっていないようです。

4. フォームのカスタマイズ

フォームのカスタマイズステップでは Drupal 7 まででお馴染みのフックを使って既存のフォームを改変する方法を見てみました。 Form ID としてフォームクラスが定義する getFormId() メソッドの戻り値を使うという点が Drupal 7 とは異なりますが、このあたりについてはほぼほぼ Drupal 8 も Drupal 7 と同じ使い方とみて問題ないようです。

今回は biwako モジュールが定義したフォームクラスを biwako モジュール自身が変更するというあまり意味のない処理をしましたが、このあたりはコアやコントリビュートモジュールが提供するフォームをコードでカスタマイズする上で必須になってくるのでぜひしっかりと押さえておきたいところです。

おまけ: Ajax 機能

最後に、フォームに Ajax 処理機能を追加しました。 Ajax API を使ったフォームへの Ajax 処理機能のステップは大きく 3 つのステップです。

  1. 既存のフィールドに #ajax 要素を追加
  2. 書き換え対象の HTML を追加
  3. Ajax コールバックを作成

まずは既存のフィールドに #ajax 要素を追加します。

'#ajax' => array(
  'callback' => array($this, 'submitFormAjax'),
  'event' => 'change',
  'progress' => array(
    'type' => 'throbber',
    'message' => '計算中',
  ),
),

この例だと、 callback で指定した関数なりメソッドが Ajax リクエストの処理を受け持ちます。 今回はフォームクラスのインスタンスメソッド submitFormAjax() を使いたいので array($this, 'submitFormAjax') と指定しました。

続いて書き換え対象の HTML を追加します。 該当するコードは以下の部分です。

$form['result'] = array(
  '#type' => 'markup',
  '#prefix' => '<div id="biwako-biwakosen-result">',
  '#suffix' => '</div>',
);

$form['counter'] = array(
  '#type' => 'markup',
  '#prefix' => '<div id="biwako-biwakosen-counter">',
  '#suffix' => '</div>',
);

いずれも #prefix #suffix を使ってラッパー div を作り ID を追加しておきます。

最後に Ajax リクエストの処理を担ういわゆる Ajax コールバックを定義します。 該当するコードは以下の部分です。 引数にはサブミットハンドラと同じものを渡します。

/**
 * BiwakosenForm の Ajax コールバック
 */
public function submitFormAjax(array &$form, FormStateInterface $form_state) {

  // 中略...

  $result = array(
    '#type' => 'markup',
    '#markup' => $message,
    '#prefix' => '<div id="biwako-biwakosen-result">',
    '#suffix' => '</div>',
  );

  // 中略...

  $counter = array(
    '#type' => 'markup',
    '#markup' => 'カウンタ: ' . $storage['count'],
    '#prefix' => '<div id="biwako-biwakosen-counter">',
    '#suffix' => '</div>',
  );

  $response = new AjaxResponse();
  $response->addCommand(new ReplaceCommand('#biwako-biwakosen-result', $result));
  $response->addCommand(new ReplaceCommand('#biwako-biwakosen-counter', $counter));

  return $response;
}

Drupal 7 では普通の連想配列だった部分が AjaxResponseReplaceCommand など独自のクラスを使うスタイルに変更されています。

このやり方はどちらかというと複雑なことができるアプローチです。 もっとシンプルに #ajaxwrapper を渡して書き換え対象を buildForm() の方で先に定義してしまう方法も利用できます。 このあたり詳しくは以下のページが参考になるので興味のある方はご覧になってみてください。

おわりに

以上です。

今回は Drupal Advent Calendar 2015 の一環として Drupal 8 のフォームの作り方をざっくりとご紹介してみました。 いかがだったでしょうか?

情報がまだまだ少ない Drupal 8 ですが、この Form API ひとつ取って見るだけでもおもしろく、この短い開発期間の間によくもこれだけ進められたなぁと呆れるぐらいの進化ぶりとなっています。 掘り下げるところがたくさんあるので、興味のある方はぜひいっしょに Drupal ウェイを極めていきましょう。 実際のプロジェクトで Drupal 8 を利用する日はまだ少し先にはなりそうですが、引き続き少しずつ調べてはできるところからシェアしていきたいと思います。

・・・ 2015 年もあっという間に残すところあと 10 日になりました。あと少しで 2016 年ですね。

2015 年はおかげさまで数多くの挑戦の機会をいただき、ウミにとって大きな飛躍の 1 年となりました。 ただ、個人的には年の始めから年末まで終始バタバタしてしまい、うれしい気持ちと同じくらい苦い思いも強く噛み締めた厳しい 1 年でもありました。

いろんな方からの気づきや学び、サポートやうれしいお言葉をいただきながら、ウミと私はまた大きく成長できたように思います。 個人でもスタジオ・ウミでも、今後も Drupal のまわりで引き続きがんばっていきたいと考えておりますので、みなさま 2016 年もどうぞよろしくお願いいたします。

みなさまよいお年を迎えくださいませ。

アドベントカレンダー、明日の担当はキノさんです。


共に働く新しい仲間を
募集しています

スタジオ・ウミは「Drupal」に特化したサービスを提供する Drupal のエキスパートチーム。
フルリモート&フレックス制だから、働く場所を選ばず時間の使い方も自由です。
そんなワークライフバランスの整った環境で、当ブログに書かれているような
様々な技術を共に学びながら、Drupalサイト開発に携わってみたい方を募集しています。
まずはお話だけでも大歓迎!ぜひお気軽にご連絡ください。