Piece Frameworkによるブログアプリケーションの作成

第6回画面設計とバリデーション定義(1)

前回までの作業によって、ブログアプリケーションを実行環境で動作させるところまで進めることができました。今回から2回に渡って、画面設計とバリデーション定義を行いアプリケーションを最終形に近づけていきます。具体的には、各画面の入力項目や表示項目を検討し、その結果をHTMLテンプレートに反映します。さらに、入力値の簡単なバリデーションを行い、その結果によってページフローが分岐するようにフロー定義ファイルの内容を変更します。これらの作業の目的は、画面の項目の洗い出しとページフローの見直しです。

Piece_Unity_Component_Flexy 1.0.0から1.1.0へのアップグレード

今回からはPiece_Unity_Component_Flexy 1.1.0の機能を使いますので、作業を始める前にアップグレードを行いましょう。

Piece_Unity_Component_Flexyのアップグレード
> pear.bat upgrade piece/piece_unity_component_flexy
...
> pear.bat list -a
INSTALLED PACKAGES, CHANNEL __URI:
==================================
(no packages installed)
INSTALLED PACKAGES, CHANNEL PEAR.PHP.NET:
=========================================
PACKAGE             VERSION STATE
Archive_Tar         1.3.2   stable
Cache_Lite          1.7.2   stable
Console_Getopt      1.2.3   stable
HTML_Template_Flexy 1.2.5   stable
Net_URL             1.0.15  stable
PEAR                1.6.2   stable
Structures_Graph    1.0.2   stable
INSTALLED PACKAGES, CHANNEL PEAR.PIECE-FRAMEWORK.COM:
=====================================================
PACKAGE                                          VERSION STATE
Piece_Examples_Basics                            1.0.0   stable
Piece_Flow                                       1.13.0  stable
Piece_Right                                      1.7.0   stable
Piece_Unity                                      1.1.0   stable
Piece_Unity_Component_Authentication             0.13.0  beta
Piece_Unity_Component_Flexy                      1.1.0   stable
Piece_Unity_Component_NullByteAttackPreventation 1.0.0   stable
Stagehand_FSM                                    1.9.0   stable
INSTALLED PACKAGES, CHANNEL PECL.PHP.NET:
=========================================
(no packages installed)

では、早速新規エントリー入力フローから手を付けていきましょう。

入力項目の検討とHTMLテンプレートの変更-新規エントリー入力画面

このフローは新規エントリー入力画面と新規エントリー入力確認画面のふたつを持っています。新規エントリー入力画面は、ブログに新規エントリーを投稿する際に使われる画面となります。今回は、ブログのエントリーとして最低限必要な項目と思われる「タイトル」「内容」を準備することにします。新規エントリー入力画面のHTMLテンプレートは下記のようになります。

/path/to/pieceblog/web/webapp/templates/Entry/New.html
<h4 class="date-header">New</h4>
<form name="New" id="New">
  <input type="hidden" name="{__flowExecutionTicketKey}" value="{__flowExecutionTicket}" id="flowExecutionTicket" />
  <p>
    Title: <input type="text" name="title" id="title" />
  </p>
  <p>
    Content: <textarea name="content" id="content"></textarea>
  </p>
  <p>
    <input type="submit" name="{__eventNameKey}_DisplayNewConfirmFromDisplayNew" value="Create" />
  </p>
</form>

先述のとおり、フロー定義に基づいた動作を行うエントリポイントに対しては、実行中のフローを特定するための情報であるフロー実行チケットとイベントを与える必要があります。上記フォームの場合は、hidden要素にフロー実行チケット、submit要素のname属性値にイベントを埋め込むことで対応しています。Piece_Unityの実行環境にイベントを与える方法はふたつあります。ひとつは、リクエストパラメータの名称にイベント名のキー、リクエストパラメータの値にイベント名を設定する方法です。もうひとつは、リクエストパラメータの名称に「イベント名のキー+"_"(アンダースコア)+イベント名」を設定する方法です。前者は、エントリポイントへのリンクに、後者はフォームに使われます。今回はフォームなので、{__eventNameKey}_DisplayNewConfirmFromDisplayNewをsubmit要素のname属性値として設定しています。

また、上記フォームには、__flowExecutionTicketKey, __flowExecutionTicket, __eventNameKeyという3種類のテンプレート変数が使われています。これらはビルトインビューエレメントと呼ばれる、Piece_Unityの実行環境が自動的に設定するテンプレート変数です。

ビルトインビューエレメント
名称
__flowExecutionTicketKeyフロー実行チケットとして認識されるリクエストパラメータの名称
__flowExecutionTicketフロー実行チケット
__eventNameKeyイベント名として認識されるリクエストパラメータの名称

ビルトインビューエレメントは上記の他にも多数用意されています。詳細は、Piece_Unityユーザーズマニュアルビューエレメントの項を参照ください。

さて、上記の準備が完了したらブラウザを起動しエントリポイントにアクセスしてみましょう。新規エントリー入力画面のCreateボタンをクリックすると、今までどおり新規エントリー入力確認画面に遷移するはずです。

新規エントリー入力画面
新規エントリー入力画面

この画面はこれで完了といきたいところですが、まだ不完全な部分が残っています。下記はブラウザに表示されたHTMLのソースからform要素部分を抜粋したものです。

新規エントリー入力画面のHTMLソース
...
<form name="New" id="New">  <input type="hidden" name="_flowExecutionTicket" value="9ad59006046cf7ef42281c74ffe9a1e9d3148a38" id="flowExecutionTicket" />
  <p>
    Title: <input type="text" name="title" id="title" />  </p>
  <p>
    Content: <textarea name="content" id="content"></textarea>  </p>
  <p>

    <input type="submit" name="_event_DisplayNewConfirmFromDisplayNew" value="Create" />
  </p>
</form>
...

見てのとおり、form要素の必須属性であるactionが見あたりません。HTMLテンプレートに直接記述することも可能ですが、今回レンダラとして使われているテンプレートエンジンHTML_Template_Flexyでは、属性をPHPコードから設定することができるので、アクションクラスを使って設定してみましょう。

イベントハンドラの設定-新規エントリー入力画面

PHPコードからform要素の属性を設定するには、HTMLがレンダリングされる前にそのコードが呼び出される必要があります。今回のように、フロー定義に基づいた動作を行うエントリポイントによって駆動されるアプリケーションでは、ビューステートのactivity要素にイベントハンドラを設定するのが妥当です。

/path/to/pieceblog/web/webapp/config/flows/Entry/New.yaml
firstState: DisplayNew

lastState:
  name: DisplayNewFinish
  view: http://example.org/list.php

viewState:

  - name: DisplayNew
    view: New
    activity:
      class: Entry_NewAction
      method: doActivityOnDisplayNew
    transition:
      - event: DisplayNewConfirmFromDisplayNew
        nextState: DisplayNewConfirm

  - name: DisplayNewConfirm
    view: NewConfirm
    transition:
      - event: DisplayNewFinishFromDisplayNewConfirm
        nextState: DisplayNewFinish
      - event: DisplayNewFromDisplayNewConfirm
        nextState: DisplayNew

activity要素には、そのステートから他のステートへの遷移が発生せずに、そのステートに留まった場合に実行されるイベントハンドラを設定することができます。イベントハンドラは、イベントが発生した場合に呼び出されるメソッドです。class要素にはアクションクラス名、method要素にはメソッド名を記述します。上記の場合、フロー起動直後にDisplayNewステートへの遷移が発生し、遷移イベントが存在しないためDisplayNew ステートに留まることになります。よって、New.htmlのレンダリング前にEntry_NewActionクラスのメソッド doActivityOnDisplayNew()がコールされることになります。

アクションクラスの作成-新規エントリー入力フロー

Piece_Unityを使ったアプリケーションは、フロー定義に基づいた状態を持つアプリケーション(ステートフルアプリケーションコンポーネント)と、それ以外の状態を持たないアプリケーション(ステートレスアプリケーションコンポーネント)を組み合わせて構成されます。ステートフルアプリケーションコンポーネントとステートレスアプリケーションコンポーネントはそれぞれ異なるアクションクラスの命名規約を持っています。

ステートフルアプリケーションコンポーネントの場合、⁠フロー名+Action」がアクションクラス名となります。フロー名は、フロー定義ファイル名から拡張子を取り除いたものです。イベントハンドラの設定でクラス名が与えられた場合は、⁠与えられたクラス名+Action」がアクションクラス名となります。

ステートレスアプリケーションコンポーネントの場合、⁠イベント名+Action」がアクションクラス名となります。

今回はステートフルアプリケーションコンポーネントのため、クラス名を省略するとアクションクラス名はNewActionとなりますが、アクションクラスのレイアウトの階層化を考慮して明示的に設定しています。

例えば、ブログのエントリーがentryテーブルに対応しているとします。データベースのひとつのテーブルに対してentryテーブルのように3つのフローを用意すると、ひとつのテーブルにつき3つのアクションクラスができることになります。階層化を考慮しない場合、すべてのアクションクラスが並列で配置されるので、テーブル数が多くなるにつれて対象のアクションを見つけるのが手間になってくるでしょう。テーブル毎にディレクトリを用意し、そこにそれぞれのフローに対応するアクションクラスを配置すれば、テーブル数が多くなっても問題にならなくなります。下記は想定している最終的なアクションクラスのレイアウトの階層です。

アクションクラスのレイアウトのテーブル毎の階層化
[プロジェクトディレクトリ (/path/to/pieceblog)]
  |
  +- [web]
       |
       +- [webapp]
            |
            +- [actions]
                 |
                 +- [Entry]
                      |
                      +- NewAction.php  - 新規エントリー入力フローに対応するEntry_NewActionクラス
                      |
                      +- ListAction.php - エントリー一覧フローに対応するEntry_ListActionクラス
                      |
                      +- EditAction.php - エントリー編集フローに対応するEntry_EditActionクラス
/path/to/pieceblog/web/webapp/actions/Entry/NewAction.php
<?php
require_once 'Piece/Unity/Service/FlowAction.php';
require_once 'Piece/Unity/Service/FlexyElement.php';

class Entry_NewAction extends Piece_Unity_Service_FlowAction
{
    function doActivityOnDisplayNew()
    {
        $flexyElement = &new Piece_Unity_Service_FlexyElement();
        $flexyElement->addForm($this->_flow->getView(), $this->_context->getScriptName());
    }
}
?>

ステートフルアプリケーションコンポーネントのアクションクラスは、Piece_Unity_Service_FlowActionクラスを継承したサブクラスとして定義されます。ステートフルアプリケーションコンポーネントでは、1つのアクションクラスは1つのフローに対応します。また、レンダラにRenderer_Flexyプラグイン(HTML_Template_Flexyを使う場合のレンダラ)を使っている場合、上記のようにPiece_Unity_Service_FlexyElementクラスを使うことでHTMLフォームの操作がいたって簡単に行えます。

さて、上記の準備が完了したらエントリポイントにアクセスしてみましょう。無事に画面が表示されたら、HTMLのソースを確認してください。

新規エントリー入力画面のHTMLソース
...
<form name="New" id="New" action="/new.php" method="post" enctype="application/x-www-form-urlencoded">
...

今回はきちんとaction要素が出力されています。これで、新規エントリー入力画面はひとまず完成とします。

表示項目の検討とHTMLテンプレートの変更-新規エントリー入力確認画面

引き続き、新規エントリー入力確認画面を考えましょう。新規エントリー入力確認画面は、新規エントリー入力画面で入力された内容を表示し、ブログへの新規エントリーの投稿を確定する際に使われる画面となります。では、新規エントリー入力画面で入力された「タイトル」「内容」を表示するようにHTMLテンプレートを作成してみましょう。

/path/to/pieceblog/web/webapp/templates/Entry/NewConfirm.html
<h4 class="date-header">NewConfirm</h4>
<p>Title: {entry.title}</p>
<p>Content: {entry.content}</p>
<form name="NewConfirm" id="NewConfirm">
  <input type="hidden" name="{__flowExecutionTicketKey}" value="{__flowExecutionTicket}" />
  <p>
    <input type="submit" name="{__eventNameKey}_DisplayNewFromDisplayNewConfirm" value="Back" />
    <input type="submit" name="{__eventNameKey}_DisplayNewFinishFromDisplayNewConfirm" value="Create" />
  </p>
</form>

上記の準備が完了したらエントリポイントにアクセスし、新規エントリー入力画面のtitleフィールドとcontentフィールドに何らかの値を入力したうえでCreateボタンをクリックしてみましょう。また、この画面のBackボタン、Createボタンがきちんと動作するかどうか確かめてみてください。

新規エントリー入力確認画面
新規エントリー入力確認画面

動作を確かめたところ、ボタンクリックによる遷移は問題なく動作しますが、入力した値が表示されません。次は、入力した値が表示されるようにしてみます。

バリデーション定義による入力値の保存

ステートフルアプリケーションコンポーネントでは、複数ページ間のデータの受け渡しにフォームのhidden要素ではなくアクションクラスのプロパティを使用します。Piece_Unityのバリデーションシステムには、バリデーション定義によって定義された入力項目の値を任意のオブジェクトにマッピングする機能があります。ユーザがアクションクラスのプロパティに入れ物となるオブジェクトを準備し、そのプロパティを引数にバリデーションを実行することで、定義された入力項目の値がそのプロパティに保存されます。この時、バリデーション定義にない項目とその値は単に捨てられます。

では、バリデーションを実装してみましょう。今回のバリデーションは新規エントリー入力画面のCreateボタンがクリックされた時に行われる必要があります。また、本物のアプリケーションでは、バリデーションの結果によって遷移先の画面を変更する必要があります。すなわち、バリデーション成功時は新規エントリー入力確認画面へ遷移し、失敗時は新規エントリー入力画面へ遷移しなければならないということです。これらを考慮すると、Createボタンがクリックされた時に遷移するステートを新規に追加する必要が出てきます。

新規エントリー入力フローのステートチャート図
新規エントリー入力フローのステートチャート図

新たに追加された青色のステートProcessValidateNewは、バリデーションの実行と、実行結果による分岐を担当するステートです。このステートはビュー(画面)を持たないため、フロー定義ファイルではactionStateの要素として表現することができます。

/path/to/pieceblog/web/webapp/config/flows/Entry/New.yaml
firstState: DisplayNew

lastState:
  name: DisplayNewFinish
  view: http://example.org/list.php

viewState:

  - name: DisplayNew
    view: New
    activity:
      class: Entry_NewAction
      method: doActivityOnDisplayNew
    transition:
      - event: ProcessValidateNewFromDisplayNew
        nextState: ProcessValidateNew

  - name: DisplayNewConfirm
    view: NewConfirm
    activity:
      class: Entry_NewAction
      method: doActivityOnDisplayNewConfirm
    transition:
      - event: DisplayNewFinishFromDisplayNewConfirm
        nextState: DisplayNewFinish
      - event: DisplayNewFromDisplayNewConfirm
        nextState: DisplayNew

actionState:

  - name: ProcessValidateNew
    activity:
      class: Entry_NewAction
      method: doActivityOnProcessValidateNew
    transition:
      - event: DisplayNewConfirmFromProcessValidateNew
        nextState: DisplayNewConfirm
      - event: DisplayNewFromProcessValidateNew
        nextState: DisplayNew

強調表示部分は変更点を表しています。基本的にはステートチャート図をそのまま再現していますが、新規エントリー入力画面と同じく新規エントリー入力確認画面にもform属性値を設定する必要があるため、DisplayNewConfirmステートにactivity要素を追加しています。なお、actionState要素の構造は、各配列要素がview要素を持たないことを除いてviewState要素とまったく同じです。

次に、この変更をアクションクラスに反映します。

/path/to/pieceblog/web/webapp/actions/Entry/NewAction.php
<?php
require_once 'Piece/Unity/Service/FlowAction.php';
require_once 'Piece/Unity/Service/FlexyElement.php';

class Entry_NewAction extends Piece_Unity_Service_FlowAction
{
    var $_entry;

    function Entry_NewAction()
    {
        $this->_entry = &new stdClass();
    }

    function doActivityOnDisplayNew()
    {
        $flexyElement = &new Piece_Unity_Service_FlexyElement();
        $flexyElement->addForm($this->_flow->getView(), $this->_context->getScriptName());
        $flexyElement->restoreValues('New', $this->_entry);
    }

    function doActivityOnProcessValidateNew()
    {
        $validation = &$this->_context->getValidation();
        if ($validation->validate('New', $this->_entry)) {
            return 'DisplayNewConfirmFromProcessValidateNew';
        } else {
            return 'DisplayNewFromProcessValidateNew';
        }
    }

    function doActivityOnDisplayNewConfirm()
    {
        $flexyElement = &new Piece_Unity_Service_FlexyElement();
        $flexyElement->addForm($this->_flow->getView(), $this->_context->getScriptName());

        $viewElement = &$this->_context->getViewElement();
        $viewElement->setElementByRef('entry', $this->_entry);
    }
}
?>

強調表示部分は変更点を表しています。$_entryプロパティは、入力値を保持するための入れ物となるstdClassオブジェクトです。新規に追加されたProcessValidateNewステートのアクティビティに対応するイベントハンドラdoActivityOnProcessValidateNew()では、バリデーションの実行と、実行結果による分岐が行われています。実行環境によって$_contextプロパティに設定されたPiece_Unity_Contextオブジェクトから、バリデーションを担当するPiece_Unity_Validationオブジェクトを取得し$validation->validate()をコールすることで、入力値のバリデーションを行うことができます。$validation->validate()の引数に$_entryを渡すことにより、バリデーションシステムはリクエストパラメータをオブジェクトにマッピングします。

また、このイベントハンドラは戻り値を返します。$validation->validate()の戻り値がtrueであればDisplayNewConfirmFromProcessValidateNewイベントが、falseであればDisplayNewFromProcessValidateNewイベントがそれぞれ返されます。このようにアクティビティのイベントハンドラからは、イベントを返すことができます。イベントを返すことで、現在のステートに対してイベントを発生させ、適切なステートに遷移させることができます。イベントハンドラを設定できる要素はイベントハンドラ要素と呼ばれ、activityの他にも多数存在します。しかし、戻り値としてイベントを返すことができるイベントハンドラはアクティビティ(activity)と遷移アクション(action)に限られています。このあたりの詳細はPiece_Flowユーザーズマニュアルフロー定義ファイルの項を参照ください。

DisplayNewConfirmステートのアクティビティに対応するイベントハンドラdoActivityOnDisplayNewConfirm()では、doActivityOnDisplayNew()と同様にform要素の設定が行われています。また、DisplayNewConfirm.htmlからの参照用に、$_entryプロパティがビューエレメントとして設定されています。

doActivityOnDisplayNew()に追加された$flexyElement->restoreValues()のコールは、$validation->validate()に対応しており、バリデーションセット"New"の入力値を新規エントリー入力画面のフォームに復元します。

バリデーション定義ファイルの基本は保存したいフォームのフィールド名を並べることです。上記の場合は、titleフィールドとcontentフィールドを保存することを表しています。

/path/to/pieceblog/web/webapp/config/validations/Entry/New.yaml
- name: title

- name: content

バリデーション定義ファイルの基本は保存したいフォームのフィールド名を並べることです。上記の場合は、titleフィールドとcontentフィールドを保存することを表しています。

最後に、エントリポイントに設定を追加します。これもアクションクラスと同様にバリデーション定義ファイルのレイアウトの階層化を考慮したものです。このあたりの設定は、アプリケーションの規模がある程度大きくなってくると必須といえるため、フレームワーク側での対応が望まれるところです。

/path/to/pieceblog/web/htdocs/new.php
<?php
error_reporting(E_ALL);

require_once 'Piece/Unity.php';
require_once 'Piece/Unity/Error.php';

Piece_Unity_Error::pushCallback(create_function('$error', 'var_dump($error); return ' . PEAR_ERRORSTACK_DIE . ';'));

$base = dirname(__FILE__) . '/../webapp';

ini_set('session.cookie_path', str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME'])));
session_save_path("$base/sessions");

$unity = &new Piece_Unity("$base/config", "$base/cache");
$unity->setConfiguration('Dispatcher_Continuation', 'flowName', 'EntryNew');
$unity->setConfiguration('Renderer_Flexy', 'templateDir', "$base/templates/Entry");
$unity->setConfiguration('Renderer_Flexy', 'compileDir', "$base/compiled-templates/Entry");
$unity->setConfiguration('Configurator_Validation', 'configDirectory', "$base/config/validations/Entry");
$unity->setConfiguration('Configurator_Validation', 'cacheDirectory', "$base/cache/validations/Entry");
$unity->dispatch();
?>

cacheDirectoryに設定したディレクトリを実際に準備する必要があることに注意してください。ここまでの作業が完了したら、ブラウザからエントリポイントにアクセスし、動作を確認してみましょう。

新規エントリー画面
新規エントリー入力画面
新規エントリー入力確認画面
新規エントリー入力確認画面

今度は入力値がきちんと保持されていることが確認できます。新規エントリー入力確認画面のBackボタンをクリックしても、新規エントリー入力画面の入力値が復元されます。このように、Piece_Unityでは特に何もしなくてもアクションクラスのプロパティの値が保持されます。ステートフルアプリケーションコンポーネントでは、アクションクラス自体がステートフルになるのです。

さて、あとは微調整です。新規エントリー入力確認画面のcontentの改行がbr要素として出力されるようにしてみましょう。

/path/to/pieceblog/web/webapp/actions/Entry/NewAction.php
<?php
...
function displayTextArea($value)
{
    return strip_tags(nl2br($value), '<br>');
}
?>
/path/to/pieceblog/web/webapp/actions/Entry/NewAction.php
<h4 class="date-header">NewConfirm</h4>
<p>Title: {entry.title}</p>
<p>Content: {GLOBALS.displayTextArea(entry.content):h}</p> 
...

これで、contentの改行が再現できました。

新規エントリー入力確認画面
新規エントリー入力確認画面

バリデーション結果によるページフローの分岐

さて、現在は何を入力してもバリデーションが成功します。このため、バリデーション失敗時の新規エントリー入力画面への遷移が確認できません。これを行う最低限の設定を下記に示します。

/path/to/pieceblog/web/webapp/config/validations/Entry/New.yaml
- name: title
  required:
    message: title is required

- name: content
  required:
    message: content is required

required要素は、そのフィールドを必須にしたり、明示的に必須でなくしたりするための要素です。上記のように、titleフィールドとcontentフィールドを必須にすることで、バリデーション失敗時の遷移を確認することができます。さらに、HTMLテンプレートにエラーメッセージを表示させるようにしましょう。

/path/to/pieceblog/web/webapp/templates/Entry/New.html
<h4 class="date-header">New</h4>
<form name="New" id="New">
  <input type="hidden" name="{__flowExecutionTicketKey}" value="{__flowExecutionTicket}" id="flowExecutionTicket" />
  <p class="error" flexy:if="__NewResults.isError(#title#)">{__NewResults.getErrorMessage(#title#)}</p>
  <p>
    Title: <input type="text" name="title" id="title" />
  </p>
  <p class="error" flexy:if="__NewResults.isError(#content#)">{__NewResults.getErrorMessage(#content#)}</p>
  <p>
    Content: <textarea name="content" id="content"></textarea>
  </p>
  <p>
    <input type="submit" name="{__eventNameKey}_ProcessValidateNewFromDisplayNew" value="Create" />
  </p>
</form>

Piece_Unityのバリデーションシステムは、バリデーション実行時にその結果を保持するPiece_Right_Resultsオブジェクトをビューエレメント「"__"(アンダースコア2つ)+バリデーションセット名+Results」として設定します。これにより、HTMLテンプレートから適切なビューエレメント名を指定することで、フィールド毎のエラーの有無やエラーメッセージを取得することができます。

おわりに

以上で、新規エントリー入力フローのふたつの画面の設計とバリデーション定義はひとまず完了です。また、これらの作業の結果として、新規エントリー入力フロー自体を見直すこともできました。冒頭で述べたように、これらの作業の目的は、あくまでも画面の項目の洗い出しとページフローの見直しであるため、ここでは詳細なバリデーション定義は行いません。

また、今回はPiece_Unityのステートフルな特性の具体例であるアクションクラスのプロパティ値の自動的な保持や、バリデーションシステムを紹介しました。これらはPiece_Unityを使ったアプリケーションの開発にとって非常に重要な要素なので十分に理解して頂きたいと思います。

次回は、残りのふたつのフロー、エントリー一覧フローとエントリー編集フローの画面設計とバリデーション定義を行います。

おすすめ記事

記事・ニュース一覧