Technote

by sizuhiko

CakePHPアプリケーションをBehatでテストする

はじめに

Behatとは、Ruby on Railsでは有名なBDDフレームワークcucumberのPHP版クローンです。以前私が発表した資料が過去記事にありますので、詳しくはそちらを参照ください。

利点は、顧客が理解できるシナリオを自然言語(つまり日本語)で記述し、それ自体がテスト実行可能であるということです。

今までCakePHPアプリケーションのテストは単体テストではSimpleTest、ブラウザベースのテストはSeleniumを使うことが多かったと思いますが、これからはBehat/Minkによってテストの幅が広がるでしょう。

しかし、BehatはSymfonyベースで、これまでCakePHPのアプリケーションを実行するためのプラグインなどは準備されてきませんでした。そこで、「rake cucumber」に習い「cake behat」として実行できるシェルタスクを用意しました。

それが「CakeBehat」シェルです。

https://github.com/sizuhiko/CakeBehat

インストール

前提事項

  1. gitが利用可能なこと
  2. PHPUnitがインストールされていること
  3. CakePHPがインストールされていること
  4. MySQLなどデータベースがインストールされていて、テスト用データベースが準備されていること

導入

CakePHPのvendorsディレクトリへ移動して、以下のコマンドを実行してください。

git clone git@github.com:sizuhiko/CakeBehat.git
git clone git://github.com/Behat/Behat.git && cd Behat
git submodule update --init
cd ..
git clone git://github.com/Behat/Mink.git && cd Mink
git submodule update --init

すると、vendors 配下に CakeBehat, Behat, Mink の3つのディレクトリができているはずです。

次に、CakeBehat/vendors/shells から behat.php と behat.yml.default を CakePHPのvendors直下へコピーします。

すると、vendors直下には、behat.php, behat.yml.default, CakeBehat, Behat, Minkがあるはずです。

続いて、CakeBehat/features を CakePHPのappやcakeと同じディレクトリ階層にコピーします。

初期導入は以上で終了です。

環境設定

Behat/Mink環境設定

vendors直下にコピーしたbehat.yml.defaultをbehat.ymlにコピーします。

4行目に

      start_url: http://test.localhost:8888/application-name/

という設定があります。これはアプリケーションのルートパスを設定するもので、ホスト名、ポート番号、アプリケーション名などを指定します。ホスト名はできるだけtest環境と識別可能なものにしておく事がオススメです。

これでおわり?

基本的な環境設定は、ここまでで、behatは実行可能な状況になっています。では具体的にサンプルアプリケーションのコードを使って解説します。

サンプルアプリケーション(ブログ)

CakeBahet/sample の中にCookBookの「CakePHPブログチュートリアル」の初期状態(Postの一覧と詳細だけ)のコードを準備しました。

データベースの切り替え

config/database.phpはそのまま生成すると$defaultがあります。UnitTestを実施する場合などは$testを作成していることもあるでしょう。Behat/Minkはブラウザアクセスで実行されるので、工夫が必要になります。またテストデータの投入で$testを参照するので、必ず定義が必要になります。 そこで Bakery の「Easy peasy database config」にも書いてるように、環境によってデータベース設定が切り替えられるように対応します。

このサンプルでは、アクセスされたサーバ名を基準に切り替えるようにしており、test.localhost なら test用データベース、それ以外ならdevelopment用データベースを利用するようにしています。

hostsファイルに 127.0.0.1 localhost, test.localhost としておけば、test.localhostでアクセス可能になるでしょう。簡単ですね。

テストシナリオの記述

Behatのシナリオ記述は基本的にcucumberと同じです。もし基本的な記述方法がわからない場合は、達人出版社「はじめる! Cucumber」を読むと良いと思います。

ブログチュートリアルの一覧表示と、詳細表示のテストとして、以下のようなシナリオを書きました。

sample/features/posts.features

# language: ja
フィーチャ: ブログの記事を閲覧した
  なぜならブログの記事を閲覧することで、最新の情報を入手したいからだ

  背景:
    前提 ブログ記事に以下の内容が登録されていること:
      | タイトル | 本文 |
      | タイトル | これは、記事の本文です。 |
      | またタイトル | そこに本文が続きます。 |
      | Title strikes back | こりゃ本当にわくわくする!うそ。 |

  シナリオ: 記事一覧を閲覧できること
    前提 トップページ を表示している
    ならば "タイトル" と表示されていること
    かつ "またタイトル" と表示されていること
    かつ "Title strikes back" と表示されていること

  シナリオ: 記事の本文を閲覧できること
    前提 トップページ を表示している
    かつ "またタイトル" のリンク先へ移動する
    ならば "そこに本文が続きます" と表示されていること

どうですか?最低限の決まりがあるように見えますが、普通のドキュメントとして読む事も可能ですよね?少なくともプログラマでなければ読めないSeleniumのようなテストコードとは違うと思います。これがBehatの特徴でもあります。

Hint

Behat/Minkでどんなステップ記述が利用可能かどうかは、

cake/console/cake behat --steps --lang ja

のように実行すると一覧表示されます。

テストデータの登録

サンプルでは「ブログ記事に以下の内容が登録されていること:」のように記述しました。これは最初から用意されているものではなく、独自に定義する必要があるステップです。ステップは features/steps の下に phpファイルを作成すれば自動的に読み込まれ、利用可能になります。

sample/features/steps/posts_step.php

$steps->Given('/^ブログ記事に以下の内容が登録されていること:$/', function($world, $table) {
  $hash = $table->getHash();
  $world->truncateModel('Post');
  $post = $world->getModel('Post');
  foreach ($hash as $row) {
    $post->create(array('Post'=>array('title'=>$row['タイトル'], 'body'=>$row['本文'])));
    $post->save();
  }
});

ステップの雛形自体は、ステップが存在しないときにBehatを実行すると、以下のように画面表示されるので、そのままコピペして作成すると簡単です。

CakeBehatではテストデータを登録するのが容易になるように、Modelを取得できるようにしています。またテストデータを消去するためにtruncateできる仕組みも用意しました。

  1. データの削除:$world->truncateModel(‘Post’);
  2. モデルの取得:$post = $world->getModel('Post’);

後は、AAで書いた表をそのまま利用できる(1行目が自動的にタイトル行として解釈されています)ので Model->createとModel->saveを利用すれば、いつものCakePHPの感覚でデータを登録できるはずです。 データベースは自動的に$testで定義された宛先に接続するようになっています。

実行してみよう

cakeやappのディレクトリに移動して、以下のコマンドを実行します。

cake/console/cake behat

さいごに

基本的なアプリケーションであれば、テスト可能な状況になっていると思います。

何かあれば、CakeBehatをforkしてアップデートに参加してもらえればと思います。

現時点で気になっているのは、

  1. モデルの初期化を shells/behat.php の _loadModels() で実行しているのですが、App::objects('model’)でモデルの一覧を取得しているので、プラグインのModelまで初期化できていません。ここで初期化する理由は、Shell(CakePHP) -> Behat(Symfony) -> CakePHPのように呼び出されると、初期化されていないモデルが利用できないので、事前に全てClassRegistry::initしておくことで、この問題を回避しています。プラグイン内のモデルを使う場合は、この記述を変更して、プラグインのモデルも利用可能にする必要があります。
  2. テストデータの削除を明示的にtruncateModelしないといけない。本来は features/support/hook.php のフックポイントでtruncateModelを呼び出せるように仕込んでおきたかったのですが、フックポイントから呼び出すとSTRICTエラーが出てしまい、うまく動作しません。ということから、データ投入ステップで一度データを削除するように記述しています。
  3. GithubのReadmeとWikiがまだ未着手で。。

といったところです。

このネタで、CakeFestに応募しようと思うのですが、後9日か。。。

PHPカンファレンス関西に参加してきました

<!– more –>4/2に開催されたPHPカンファレンス関西に参加してきました。

実行委員の皆様お疲れさまでした。ものすごい楽しいイベントでした。

僕はLTでBehatの話をしたのですが、他のプレゼンテーションがとてもバラエティにとんでいて、とても勉強になったし、交流もできて良かったです。

プログラムの内容や、それぞれの内容は他の参加者の方が詳しく書いていたりするので、簡単にKPT形式で振り返っておきます。

※アンケート書いたけど、やっぱり落ち着いたところで考えると違いますね。

KEEP

  1. 前日入りは楽
  2. 会場がキレイ(税金がふんだんに投入されている感のある・・・以降自粛)
  3. 関西での大規模イベント(東京からの参加でしたが。。。)
  4. スムーズな進行、時間通りに進んでいて感動した
  5. 懇親会LTも大スクリーンで見やすい
  6. 懇親会の食事が美味しい
  7. ドラ娘がかわいい

Problem

  1. 電源が少ない。自分はAirだし、Wifiルータも10時間持つURoad-9000だったので大丈夫でしたが、全体的に困っていた人が多かった印象
  2. 欲を言えば2トラックぐらいあると良かったかも(もっとコード的な欲望がある人向けセッションみたいな)
  3. 16Fのレストランはテンパっていた w (本編に関係ないですが)
  4. 懇親会同一会場は待ち時間が微妙
  5. (本編LTで)ドラ娘の仕事が少なかった(懇親会では大変そうでした。お疲れさまでした)
  6. LT発表者が東方面だった

Try

  1. 次はハッカソン? (ザワっ
  2. 青年団も何か告知できるようにしておきたかった (謎
  3. また来年もあれば参加したい

で、スライドなんですが、いつものslideshareにアップしてたんですが、何度やっても失敗するようになったので、おさらばすることにしました(過去のファイルはそのまま置いておきますが)。取り急ぎ、MyOperaのストレージに置いたので、こちらをダウンロードして見てください。

また、懇親会のジャンケン大会で本をもらいました。

最後に、東京に帰る新幹線で、WiMAXルータURoad-9000がどのくらい使えるか検証してみました。

  1. トンネル以外は概ね問題なく接続でき、とても快適
  2. トンネル以外での不調エリアは、米原、浜名湖近辺だけ
  3. 上記いずれも体感です(ご了承ください)

ということで、N700系であればWifi使えるけど、それ以外どうする?といった場合、WiMAXも十分行けますよ、あなた。(宣伝)

Behatのインストールについての注意事項

<!– more –>4/2 に行われるPHPカンファレンス関西にて、BehatについてLTをすることになりました。

そこで最近Behatをダウンロードしてみたのですが、git submoduleでのSymfonyコンポーネントのインストールで問題がありますので、解決策を書いておきます。

  1. Behat 本体のインストール

    git clone git://github.com/Behat/Behat.git

  2. サブモジュールのインストール

    cd Behat git submodule update –init

とくに問題がなければ、しばらくして終了しますが、本稿執筆次点では以下のようなエラーで中断してしまいます。

Initialized empty Git repository in /Behat/vendor/Symfony/Component/Translation/.git/
remote: Counting objects: 220, done.
remote: Compressing objects: 100% (124/124), done.
remote: Total 220 (delta 147), reused 141 (delta 94)
Receiving objects: 100% (220/220), 52.61 KiB, done.
Resolving deltas: 100% (147/147), done.
fatal: reference is not a tree: d5cdaba8550b7b99f37d2aad345dd8ebbe3efb9c
Unable to checkout 'd5cdaba8550b7b99f37d2aad345dd8ebbe3efb9c' in submodule path 'vendor/Symfony/Component/Translation'

そこで、エラーになったサブモジュールの変更ログを確認します。

git log --oneline -p -- vendor/Symfony/Component/Translation

上記コマンドを実行すると、以下のように表示されます。

85d8dac updated vendors
diff --git a/vendor/Symfony/Component/Translation b/vendor/Symfony/Component/Translation
index f70e0ba..d5cdaba 160000
--- a/vendor/Symfony/Component/Translation
+++ b/vendor/Symfony/Component/Translation
@@ -1 +1 @@
-Subproject commit f70e0ba1b04e7db4b3aa6d122470291d4fb34732
+Subproject commit d5cdaba8550b7b99f37d2aad345dd8ebbe3efb9c
da2503a updated submodules
diff --git a/vendor/Symfony/Component/Translation b/vendor/Symfony/Component/Translation
new file mode 160000
index 0000000..f70e0ba
--- /dev/null
+++ b/vendor/Symfony/Component/Translation
@@ -0,0 +1 @@
+Subproject commit f70e0ba1b04e7db4b3aa6d122470291d4fb34732

ここで表示されたログの1行目「85d8dac updated vendors」の85d8dacを使ってチェックアウトします。

git checkout 85d8dac~ -- vendor/Symfony/Component/Translation
git submodule update

こうすることで、サブモジュールのインストールが継続できます。

  1. 実行する

    bash-3.2$ ./bin/behat.php ……………………………………………………………. ……………………………………………………………. ……………………………………………………………………….

    36 scenarios (36 passed) 222 steps (222 passed) 0m7.045s

成功したら、インストールは完了です。

PHPのBDDフレームワーク Behat について発表してきました

第55回PHP勉強会@関東で発表してきました

2011/2/10にPHP勉強会が約半年ぶりに開催されました。以前、発表するぜーと公言していたので、今回は万全を期してと思いきや最近の多忙に負けそうになりました。が、前日(というか当日朝4時)までバタバタしながら準備してました。。。

今回は昨年のPHP祭りで日本語ハックしたBehatの入門編です。あのときはLTという形であまり詳細に触れられなかったので、30分という時間ですが実際に1からやってみました。 ここでは実際に「やってみよう」で実践した手順について解説します。

はじめに

スライドの「1. インストール」でも書いていますが、まずBehat本体をインストールしてください。私がforkした日本語対応版をcloneすると「3. 日本語環境」の部分をスキップできます。 「2. テスト環境」に関しては、今回behat_webstepsを流用しました。それ以外にもGoutte-for-Behat(https://github.com/ThePixelDeveloper/Goutte-for-Behat)というものもあります。どちらもHTMLクライアントのエンジンとして「Goutte」を使っています。前者はgoutte.pharが含まれていますが、後者は含まれていないので別途Goutteをcloneする必要があります。ただ後者の方がstepsのコードは流用しやすいかもしれません。

準備しよう

まず初期状態のディレクトリを以下のようにしました。

|-- Behat Behat本体のclone
|-- behat_websteps behat_webstepsのclone

練習環境を作る

behat_webstepsを土台にして、テスト環境を作ります。といっても、まずは単にコピーするだけです。

cp -R behat_websteps sandbox

フィーチャーファイルを作成する

スライドでも引用したgihyo.jpでのcucumberについてとてもわかりやすい連載「第22回 Railsアプリの受け入れテストをCucumberで書こう」のフィーチャーを使用します。

vi sandbox/features/sample.feature

実際のコードは以下のとおりです。

# language: ja
フィーチャ: ユーザを管理したい

  シナリオ: ユーザの登録
    前提 "ユーザ登録"ページを表示している
    もし "email"に"example@example.com"と入力する
    かつ "name"に"赤松 祐希"と入力する
    かつ "age"に"22"と入力する
    かつ "Create"ボタンをクリックする
    ならば "User was successfully created."と表示されていること
    かつ "example@example.com"と表示されていること
    かつ "赤松 祐希"と表示されていること
    かつ "22"と表示されていること

ここでcucumberと違うのは1行目(フェーチャの前)に言語指定をするところです。現時点のBehatではこれを書く事でja.xmlを利用してくれます。

実行してみる

フィーチャーはbin/behatコマンドでテストします。

bash-3.2$ Behat/bin/behat sandbox/features/sample.feature 

画面には、以下のように出力されます。

<code>
フィーチャ: ユーザを管理したい

  シナリオ: ユーザの登録                                     # features/sample.feature:4

<span style="color: goldenrod">
    前提 "ユーザ登録"ページを表示している
    もし "email"に"example@example.com"と入力する
    かつ "name"に"赤松 祐希"と入力する
    かつ "age"に"22"と入力する
    かつ "Create"ボタンをクリックする
    ならば "User was successfully created."と表示されていること
    かつ "example@example.com"と表示されていること
    かつ "赤松 祐希"と表示されていること
    かつ "22"と表示されていること
</span>
1 scenario (<span style="color: goldenrod">1 undefined</span>)
9 steps (<span style="color: goldenrod">9 undefined</span>)
0.123s
<span style="color: goldenrod">
You can implement step definitions for undefined steps with these snippets:

$steps->前提('/^"([^"]*)"ページを表示している$/', function($world, $arg1) {
    throw new EverzetBehatExceptionPending();
});

$steps->もし('/^"([^"]*)"に"([^"]*)"と入力する$/', function($world, $arg1, $arg2) {
    throw new EverzetBehatExceptionPending();
});

$steps->かつ('/^"([^"]*)"ボタンをクリックする$/', function($world, $arg1) {
    throw new EverzetBehatExceptionPending();
});

$steps->ならば('/^"([^"]*)"と表示されていること$/', function($world, $arg1) {
    throw new EverzetBehatExceptionPending();
});
</span>
</code>

ステップ(テストの定義)が記述されていないので、9つの未定義エラーが表示されています。その下にはスケルトンが表示されているので、まずこれをコピペしてstepファイルを作成します。

ステップファイルの作成

stepファイル「webteststep.php」に前節のスケルトンを貼付けます。以下のようになるでしょう。

<?php

$steps->前提('/^"([^"]*)"ページを表示している$/', function($world, $arg1) {
    throw new EverzetBehatExceptionPending();
});

$steps->もし('/^"([^"]*)"に"([^"]*)"と入力する$/', function($world, $arg1, $arg2) {
    throw new EverzetBehatExceptionPending();
});

$steps->かつ('/^"([^"]*)"ボタンをクリックする$/', function($world, $arg1) {
    throw new EverzetBehatExceptionPending();
});

$steps->ならば('/^"([^"]*)"と表示されていること$/', function($world, $arg1) {
    throw new EverzetBehatExceptionPending();
});

また実行してみましょう。

<code>
bash-3.2$ Behat/bin/behat sandbox/features/sample.feature 
フィーチャ: ユーザを管理したい

  シナリオ: ユーザの登録                                     # features/sample.feature:4

<span style="color: goldenrod">
    前提 "ユーザ登録"ページを表示している                           # features/steps/web_test_step.php:5
      TODO: write pending definition
</span><span style="color: skyblue">
    もし "email"に"example@example.com"と入力する          # features/steps/web_test_step.php:9
    ...(** 省略 **)
</span>

1 scenario (<span style="color: goldenrod">1 pending</span>)
9 steps (<span style="color: skyblue">8 skipped</span>, <span style="color: goldenrod">1 pending</span>)
0.089s
</code>

未定義(undefined)から、ペンディングに変わりましたね。これは貼付けたコードが現時点でPending例外を投げているためです。

ステップを記述する

ステップの中身はbehat_webstepsのcommon.phpを流用しました。

<?php

$steps->前提('/^"([^"]*)"ページを表示している$/', function($world, $page) {
  $page = $world->__getPath($page);

  $world->client->request('GET', $page);
  $world->__getClientProperties();
});

$steps->もし('/^"([^"]*)"に"([^"]*)"と入力する$/', function($world, $field, $value) {
  assertNotNull($world->page,"No webpage loaded");
  $form = $world->__getForm('Create');
  $form[$field]->setValue($value);
});

$steps->かつ('/^"([^"]*)"ボタンをクリックする$/', function($world, $button) {
  assertNotNull($world->page,"No webpage loaded");
  $form = $world->__getForm('Create');
  $world->client->submit($form);
  $world->__getClientProperties();
});

$steps->ならば('/^"([^"]*)"と表示されていること$/', function($world, $text) {
  assertNotNull($world->page,"No webpage loaded");
  assertContains($text,$world->output);
});

実行してみましょう。

<code>
bash-3.2$ Behat/bin/behat sandbox/features/sample.feature 
フィーチャ: ユーザを管理したい

  シナリオ: ユーザの登録                                     # features/sample.feature:4

<span style="color: firebrick">    前提 "ユーザ登録"ページを表示している                           # features/steps/web_test_step.php:8
      Unknown path 'ユーザ登録'. You can define it in [features_folder]/support/paths.php
      Failed asserting that an array has the key <string:ユーザ登録>.
</span><span style="color: skyblue">    もし "email"に"example@example.com"と入力する          # features/steps/web_test_step.php:14
    ...(** 省略 **)
</span>

1 scenario (<span style="color: firebrick">1 failed</span>)
9 steps (<span style="color: skyblue">8 skipped</span>), <span style="color: firebrick">1 failed</span>)
0.355s
</code>

すると「ユーザ登録」なんてURLは見つからないというエラーになります。まぁそうですね。URLに関しては画面名と実際のhttpリクエストするURLを関連づけておく必要があります。これはgoutte以外のHTTPクライアントを使っても同じことです。ベースとしたbehat_webstepsではsupport/paths.phpにそのマッピングを書くようにしていますので、追加します。

<?php
$world->paths = array();
$world->paths['ユーザ登録'] = "http://localhost/user_regist.php";

テストを失敗させる

ここまでで、初期段階の準備は完了です。ではまたテストを実行してみましょう。

<code>
bash-3.2$ Behat/bin/behat sandbox/features/sample.feature 
フィーチャ: ユーザを管理したい

  シナリオ: ユーザの登録                                     # features/sample.feature:4

    <span style="color: green">前提 "ユーザ登録"ページを表示している                           # features/steps/web_test_step.php:8</span>
    <span style="color: firebrick">もし "email"に"example@example.com"と入力する          # features/steps/web_test_step.php:14
      The current node list is empty.</span>
    <span style="color: skyblue">...(** 省略 **)</span>

1 scenario (<span style="color: firebrick">1 failed</span>)
9 steps (<span style="color: green">1 passed</span>, <span style="color: skyblue">7 skipped</span>, <span style="color: firebrick">1 failed</span>)
0.114s
</code>

テストを実行してみると、最初に入力フォームに値をセットしようとするところで失敗します。まだユーザ登録画面を表示するプログラムはありません。

それでは実際に動作するコードを書いてみましょう。

実装する

ここではあくまでテストを通過させる簡単なコードを書いてみます。画面の入力フォームから入力された値を表示するuser_regist.phpを書いてみましょう。

<!DOCTYPE html>
<html>
    <head>
      <title>ユーザ登録</title>
    </head>
    <body>
        <div><?
            if ($_POST["submit"]) {
                echo "User was successfully created.". "<br />";
                echo "name = "     . $_POST['name']  . "<br />";
                echo "e-mail = "   . $_POST['email'] . "<br />";
                echo "age = "      . $_POST['age']   . "<br />";
            }
        ?></div>
        <form method="post" action="user_regist.php">
            <div>
                <label>名前:</label><input type="text" name="name" value="">
            </div>
            <div>
                <label>e-mail:</label><input type="text" name="email" value="">
            </div>
            <div>
                <label>年齢:</label><input type="text" name="age" value="">
            </div>
            <div>
                <input type="submit" name="submit" value="Create">
            </div>
        </form>
    </body>
</html>

※まぁなんとも安直なコードですが、ご勘弁を・・・

作成したファイルをドキュメントルートに設置してテストを実行しましょう。

<code>
bash-3.2$ Behat/bin/behat sandbox/features/sample.feature 
フィーチャ: ユーザを管理したい

  シナリオ: ユーザの登録                                     # features/sample.feature:4

<span style="color: green">    前提 "ユーザ登録"ページを表示している                           # features/steps/web_test_step.php:8
    もし "email"に"example@example.com"と入力する          # features/steps/web_test_step.php:14
    かつ "name"に"赤松 祐希"と入力する                         # features/steps/web_test_step.php:14
    かつ "age"に"22"と入力する                             # features/steps/web_test_step.php:14
    かつ "Create"ボタンをクリックする                          # features/steps/web_test_step.php:21
    ならば "User was successfully created."と表示されていること # features/steps/web_test_step.php:26
    かつ "example@example.com"と表示されていること             # features/steps/web_test_step.php:26
    かつ "赤松 祐希"と表示されていること                           # features/steps/web_test_step.php:26
    かつ "22"と表示されていること                              # features/steps/web_test_step.php:26
</span>
1 scenario (<span style="color: green">1 passed</span>)
9 steps (<span style="color: green">9 passed</span>)
0.389s
</code>

やったー、グリーンでテスト成功です。

さいごに

今回はbehatwebstepsをベースにしましたが、HTTPクライアントは何でも大丈夫です。今回goutteでしたが、BehatはPHPUnitを使うのでPHPUnitのPHPUnitExtensions_SeleniumTestCaseを使うのも1つでしょう。Javascriptが使われているサイトなどでは、こちらの選択になりますね。

大事なことはアジャイルでドキュメントを書く事と、無駄なドキュメントでなく実行可能なドキュメントである、ということですね。プレーンテキストなのでプログラマじゃなくても、お客さんでも読めるし、書いてもらう事も可能かもしれません。打ち合わせでフィーチャファイルを議事録的に書きながら進めることもできると思います。

今後の展開としては、一般的なwebstepに関してはgoutteを使った日本語版を、私のgithubアカウントで公開予定です。4/2のPHPカンファレンス関西までには公開して何かしゃべりたいなーと思っていますが、ちょっとテーマと違うか。。LT狙いで何か。。

Behat RC1も期待して待ちましょう!!

懇親会でも話題になったのですが、PHPSpecはどこへ行ってしまったのか・・・

それと、pharファイルが使えないよ!という方(当日朝3時までのオレ)、はこちらの「Pharは便利だけど --enalbe-zend-multibyteが有効だと文字化けしてしまう」を参考にしてください!!

バリデーションプラグインと組み合わせて使うと便利な、確認画面を出すjQueryプラグイン

<!– more –>これまで確認画面を出すだけのものや、入力チェックだけをするものはプラグインとして出ていたのですが、どうも組み合わせて使おうとすると不便なことが多かったので、プラグインを自作してみました。

ダウンロード:

ソースコードは以下のgithubで公開しています。

https://github.com/sizuhiko/confirmForm

デモ:

まずどのような動作をするのか、デモページで確認したほうがわかりやすいと思います。以下のURLから操作してみてください。

/demo/confirmForm/demo/index.html

デモサイトでは入力チェックにex-valudationプラグインを使っています。

必須条件:

特徴:

  • 確認画面を出すかどうかは自分で判断します
  • 確認画面でOKが選択されるまでフォームはSubmitされません
  • 一般的なフォームの形式であれば、ほとんどカスタマイズ(オプションで指定する)の必要なし

一般的なフォームって?

このプラグインでは以下のような構造のHTMLフォームが利用されることを想定しています。

<form action="posted.html" method="get" id="inputForm">
  <div>
    <label for="user_id">ID:</label>
    <input type="text" name="id" id="user_id" value=""><span class="required">*</span>
  </div>
  <div>
    <label for="user_name">Name:</label>
    <input type="text" name="name" id="user_name" value=""><span class="required">*</span>
  </div>
  <div>
    <label for="user_sex">Sex:</label>
    <fieldset id="user_sex">
      <input type="radio" name="sex" id="user_sex_male" value="1"><label for="user_sex_male">Male</label>
      <input type="radio" name="sex" id="user_sex_female" value="2"><label for="user_sex_female">Female</label>
      <span class="required">*</span>
    </fieldset>
  </div>
  <div>
    <label for="user_lang">Language:</label>
    <select name="lang" id="user_lang">
      <option value="">-- please select --</option><option value="jp">Japanese</option><option value="en">English</option>
    </select><span class="required">*</span>
  </div>
  <div>
    <label for="user_receive_news">Receive News Mail:</label>
    <fieldset id="user_receive_news">
      <input type="checkbox" name="news" id="user_reveive_news_sports" value="1"><label for="user_receive_news_sports">Sports</label>
      <input type="checkbox" name="news" id="user_reveive_news_music" value="2"><label for="user_receive_news_music">Music</label>
      <input type="checkbox" name="news" id="user_reveive_news_computer" value="3"><label for="user_receive_news_computer">Computer</label>
    </fieldset>
  </div>
  <div class="buttons">
    <input type="submit" value="next" id="submit_ok">
  </div>
</form>

これはデモページのフォーム部分そのものなのですが、項目をdivタグで囲い、その中の項目名はlabelで入力フィールドはinputやselectを使うようなデザインです。

プラグインで必要になる記述

javascriptファイルをロードします。 jqModelプラグインを使っているので、jqModal、もちろんjqueryもロードしてください。

<script charset="utf-8" type="text/javascript" src="http://code.jquery.com/jquery-1.4.2.js"></script>
<script charset="utf-8" type="text/javascript" src="http://dev.iceburg.net/jquery/jqModal/jqModal.js"></script>
<script src="../src/confirmform.js"></script>

jqModalのスタイルシートが必要になります。また、jqModalのexampleで記述されているConfirmサンプルのスタイル定義を利用しているので、サンプルHTML上から切り出したスタイルシートをvendorディレクトリに用意しました。これを使うと簡単に利用を開始することができます。

<link charset="utf-8" href="../vendor/jqModal/jqmConfirm.css" type="text/css" rel="stylesheet"></link>
<link charset="utf-8" href="http://dev.iceburg.net/jquery/jqModal/jqModal.css" type="text/css" rel="stylesheet"></link>

プラグインの初期化

プラグインを利用するためには、HTML上に1つ確認画面用の空エレメントを追加する必要があります。場所はbody内であればどこでもかまいません。

<div id="confirmForm"></div>

idの値も特に意味はありません。ここではconfirmFormという値にしておきます。

プラグインのインスタンスを初期化するには、$(“入力フォームのセレクタ”).confirmForm(“確認画面用のセレクタ”, オプション) を使います。

$(document).ready(function(){
  ...
  $("#inputForm").confirmForm("#confirmForm", {});
  ...
}

ひとまず、このようにシンプルな宣言で動作します。実際に利用する場面では、入力フォームでselectボックスやcheckbox,radioボタンなどを利用していると思います。デフォルトの挙動では値をinputのvalueから取り出して表示するだけなので、コード値を名前に変換する必要があるかもしれません。このあたりのカスタマイズは、オプション節で解説します。

確認画面を表示する

このプラグインでは確認画面は自分で表示する必要があります。たとえばバリデーションプラグインの成功時コールバックで呼び出すようなイメージです。

$("#inputForm").confirmShow();

これはとてもシンプルで、confirmShow関数を呼び出すだけです。

OKが押されたかどうかチェックする

このプラグインでは、確認画面でOKが選択されたらもう一度入力フォームでSubmitを実行します。そのため入力チェックが再実行されて、コールバックが呼び出されるかもしれません。そのためにOKが既に選択されていたらそのままreturnするような実装にすることになると思います。

validCallback:function() {
  if($("#inputForm").isConfirmed()) {
    return;
  }
  $("#inputForm").confirmShow();
}

確認済みかどうかはisConfirmed関数を呼び出すだけです。入力内容が問題ないときのコールバックがvalidCallbackだったとすると、上記のような実装になります。

オプション

実際に利用する場面では、必ずしもinputのvalue値が確認画面に出れば良い訳ではありません。また、タイトルを変更したりボタンのラベルを変更したりもしたいでしょう。そのためにプラグイン初期化時にオプションを指定できるようにしてあります。

オプション一覧:

名前意味デフォルト値概要

title タイトル Are you sure? 確認画面上部に表示されるタイトルを文字列で指定

yes はいボタンのラベル Yes 文字列で指定

no いいえボタンのラベル no 文字列で指定

findLavel ラベルを探索して文字列を返すためのコールバック 1つ前の要素のtext部分(一般的なフォームのtextボックスではlabelになる) radioやcheckboxのようにラベルがfieldsetの外側にあるようなケースで独自にコールバック関数を実装する

formatValue 値をカスタマイズするためのコールバック inputやselectのvalue値 value値がコードだが、確認画面では値(名前)で出したい場合に独自にコールバック関数を実装する

separator 複数選択値の区切り文字 ,(コンマ) 同一nameがある場合に、値を区切る文字列

ラベル文字列のコールバック

デフォルトの挙動は以下のとおりです。

'findLabel': function(inputField) {
    return $(inputField).prev().html();
}

textボックスは直前の要素がlabelであることを想定しているので、すべてが一般的なフォームのレイアウトで、textボックスしか利用していなければ、オプションの指定は必要ありません。しかし実際はlabelが右だったり1つ親の要素だったりすることがあります。 このような場面ではコールバックを実装してください。

例えばデモページでは以下のようなコードになっています。

'findLabel': function(field) {
    switch(field.name) {
    case 'sex':
        return 'Sex';
    case 'news':
        return 'Receive News Mail';
    default :
        str = $(field).prev().html();
        return str.substring(0, str.length-1);
    }
}

findLabelに関してはformatValurのように項目別ではなく、functionで受け取って独自にカスタマイズする方式を取っています。これは項目名は一括ルールで変換できる場合が想定できるためです。

※このオプションに関しては、将来formatValueのようにhashでも指定できる(併用)方式に変更する予定です。

値をカスタマイズするためのコールバック

inputやselectのvalue値は必ずしも確認表示用には向いていないこともある(例えばコード値になっているなど)ので、このようなケースではコールバック関数を実装してください。

例えばデモページでは以下のようなコードになっています。

'formatValue':{
  'sex':function(field) {
    return {1:"Male", 2:"Female"}[$(field).val()];
  },
  'lang':function(field) {
    return {'jp':"Japanese", 'en':"English"}[$(field).val()];
  },
  'news':function(field) {
    return {1:"Sports", 2:"Music", 3:"Computer"}[$(field).val()];
  }
}

formatValue自体は各入力項目のname属性をキーにしたhashで定義します。

さいごに

今後以下の機能について、バージョンアップを予定しています。

  • yes,noボタンの左右を逆に指定できるようにする
  • selectやcheckbox、radioなどタイプごとに推奨フォームを想定してラベル値を取得する
  • findLabelをhashでもオプション指定できるようにする

その他カスタマイズ要望などあれば、機能追加していきたいな、と思っています。