1 <?xml version="1.0" encoding="UTF-8"?>
3 <!-- EN-Revision: 20774 -->
4 <sect1 id="zend.form.advanced">
5 <title>Zend_Form の高度な使用法</title>
8 <classname>Zend_Form</classname> にはさまざまな機能があり、
9 その多くは熟練者向けに用意されています。本章では、
13 <sect2 id="zend.form.advanced.arrayNotation">
17 関連するフォーム要素について、要素名を配列形式にしてグループ化したいこともあるでしょう。
18 たとえば、配送先と請求先のふたつの住所を受け取りたい場合、
19 それぞれに同じ要素を使った上で配列でグループ化すれば、
21 たとえば次のようなフォームを例に考えてみましょう。
24 <programlisting language="html"><![CDATA[
29 <dt><label for="recipient">氏名:</label></dt>
30 <dd><input name="recipient" type="text" value="" /></dd>
32 <dt><label for="address">住所:</label></dt>
33 <dd><input name="address" type="text" value="" /></dd>
35 <dt><label for="municipality">市:</label></dt>
36 <dd><input name="municipality" type="text" value="" /></dd>
38 <dt><label for="province">州:</label></dt>
39 <dd><input name="province" type="text" value="" /></dd>
41 <dt><label for="postal">郵便番号:</label></dt>
42 <dd><input name="postal" type="text" value="" /></dd>
49 <dt><label for="payer">氏名:</label></dt>
50 <dd><input name="payer" type="text" value="" /></dd>
52 <dt><label for="address">住所:</label></dt>
53 <dd><input name="address" type="text" value="" /></dd>
55 <dt><label for="municipality">市:</label></dt>
56 <dd><input name="municipality" type="text" value="" /></dd>
58 <dt><label for="province">州:</label></dt>
59 <dd><input name="province" type="text" value="" /></dd>
61 <dt><label for="postal">郵便番号:</label></dt>
62 <dd><input name="postal" type="text" value="" /></dd>
67 <dt><label for="terms">I agree to the Terms of Service</label></dt>
68 <dd><input name="terms" type="checkbox" value="" /></dd>
71 <dd><input name="save" type="submit" value="Save" /></dd>
77 この例では、請求先住所と配送先住所に同じフィールドを使用しているため、
82 <programlisting language="html"><![CDATA[
87 <dt><label for="shipping-recipient">氏名:</label></dt>
88 <dd><input name="shipping[recipient]" id="shipping-recipient"
89 type="text" value="" /></dd>
91 <dt><label for="shipping-address">住所:</label></dt>
92 <dd><input name="shipping[address]" id="shipping-address"
93 type="text" value="" /></dd>
95 <dt><label for="shipping-municipality">市:</label></dt>
96 <dd><input name="shipping[municipality]" id="shipping-municipality"
97 type="text" value="" /></dd>
99 <dt><label for="shipping-province">州:</label></dt>
100 <dd><input name="shipping[province]" id="shipping-province"
101 type="text" value="" /></dd>
103 <dt><label for="shipping-postal">郵便番号:</label></dt>
104 <dd><input name="shipping[postal]" id="shipping-postal"
105 type="text" value="" /></dd>
112 <dt><label for="billing-payer">氏名:</label></dt>
113 <dd><input name="billing[payer]" id="billing-payer"
114 type="text" value="" /></dd>
116 <dt><label for="billing-address">住所:</label></dt>
117 <dd><input name="billing[address]" id="billing-address"
118 type="text" value="" /></dd>
120 <dt><label for="billing-municipality">市:</label></dt>
121 <dd><input name="billing[municipality]" id="billing-municipality"
122 type="text" value="" /></dd>
124 <dt><label for="billing-province">州:</label></dt>
125 <dd><input name="billing[province]" id="billing-province"
126 type="text" value="" /></dd>
128 <dt><label for="billing-postal">郵便番号:</label></dt>
129 <dd><input name="billing[postal]" id="billing-postal"
130 type="text" value="" /></dd>
135 <dt><label for="terms">I agree to the Terms of Service</label></dt>
136 <dd><input name="terms" type="checkbox" value="" /></dd>
139 <dd><input name="save" type="submit" value="Save" /></dd>
145 上の例では、住所をそれぞれ個別に受け取ることができます。
146 このフォームを送信すると、受け取り側では 3 つの要素を取得できます。
147 'save' が送信ボタン、そしてふたつの配列 'shipping' と 'billing'
148 の中にはさまざまなキーとそれに対応する要素が含まれています。
152 <classname>Zend_Form</classname> は、この処理を
153 <link linkend="zend.form.forms.subforms">サブフォーム</link>
155 サブフォームのレンダリングには先ほどのような配列記法を使用します。
157 配列のキーはサブフォーム内に含まれる要素となります。
158 サブフォームは、何段階でもネストさせることができます。
159 その場合も、ネストした配列形式でその構造を表します。
160 さらに、<classname>Zend_Form</classname>
161 のさまざまなバリデーション機能は、この配列構造をきちんと処理するようにできています。
162 サブフォームをどれだけ深くネストさせたとしても、
164 この機能を使うために特に何かしなければならないということはありません。
165 この機能はデフォルトで有効になっています。
170 特定の配列を指定してそこに要素やコレクションを所属させたりといった機能もあります。
176 <methodname>Zend_Form::setIsArray($flag)</methodname>:
177 このフラグを <constant>TRUE</constant> にすると、フォーム全体を配列として扱うことができます。
178 デフォルトでは、<methodname>setElementsBelongTo()</methodname>
179 がコールされていない限りはフォーム名を配列の名前とします。
181 <methodname>setElementsBelongTo()</methodname> が設定されていない場合は、
182 このフラグは無視されます (要素が属する配列の名前がないからです)。
186 フォームが配列として扱われているかどうかを知りたい場合には
187 <methodname>isArray()</methodname> アクセサを使用します。
193 <methodname>Zend_Form::setElementsBelongTo($array)</methodname>:
194 このメソッドを使用すると、フォームの全要素が属する
195 配列の名前を指定できます。現在設定されている値を調べるには
196 <methodname>getElementsBelongTo()</methodname> アクセサを使用します。
202 さらに、要素レベルでは、特定の要素を特定の配列に属させるために
203 <methodname>Zend_Form_Element::setBelongsTo()</methodname> メソッドを使うこともできます。
204 この値が何者なのか (明示的に設定されたものなのか
205 フォームを経由して暗黙的に設定されたものなのか)
206 を知るには <methodname>getBelongsTo()</methodname> アクセサを使用します。
210 <sect2 id="zend.form.advanced.multiPage">
211 <title>複数ページのフォーム</title>
215 <classname>Zend_Form</classname> では公式にはサポートしていません。
216 しかし、それを実装するための機能の大半はサポートしており、
217 ほんの少し手を加えるだけでこの機能を実現できます。
221 複数ページのフォームを作成する鍵となるのが、
222 サブフォームの活用です。各ページに、ひとつのサブフォームだけを表示させるわけです。
223 こうすれば、それぞれのサブフォームの内容を各ページで検証し、
224 かつすべてのサブフォームの入力を終えるまでフォームの処理を行わないということができます。
227 <example id="zend.form.advanced.multiPage.registration">
228 <title>登録フォームの例</title>
231 例として、登録フォームを考えてみましょう。
232 まず最初のページでユーザ名とパスワードを入力してもらい、
234 (姓、名、住所など)、そして最後のページでは
235 参加したいメーリングリストを選択するといったものです。
240 その中でサブフォームをいくつか定義します。
243 <programlisting language="php"><![CDATA[
244 class My_Form_Registration extends Zend_Form
246 public function init()
248 // ユーザサブフォーム (ユーザ名とパスワード) を作成します
249 $user = new Zend_Form_SubForm();
250 $user->addElements(array(
251 new Zend_Form_Element_Text('username', array(
253 'label' => 'Username:',
254 'filters' => array('StringTrim', 'StringToLower'),
255 'validators' => array(
259 array('/^[a-z][a-z0-9]{2,}$/'))
263 new Zend_Form_Element_Password('password', array(
265 'label' => 'Password:',
266 'filters' => array('StringTrim'),
267 'validators' => array(
269 array('StringLength', false, array(6))
274 // 詳細サブフォーム (姓、名、住所) を作成します
275 $demog = new Zend_Form_SubForm();
276 $demog->addElements(array(
277 new Zend_Form_Element_Text('givenName', array(
279 'label' => 'Given (First) Name:',
280 'filters' => array('StringTrim'),
281 'validators' => array(
284 array('/^[a-z][a-z0-9., \'-]{2,}$/i'))
288 new Zend_Form_Element_Text('familyName', array(
290 'label' => 'Family (Last) Name:',
291 'filters' => array('StringTrim'),
292 'validators' => array(
295 array('/^[a-z][a-z0-9., \'-]{2,}$/i'))
299 new Zend_Form_Element_Text('location', array(
301 'label' => 'Your Location:',
302 'filters' => array('StringTrim'),
303 'validators' => array(
304 array('StringLength', false, array(2))
309 // メーリングリストサブフォームを作成します
310 $listOptions = array(
311 'none' => 'No lists, please',
312 'fw-general' => 'Zend Framework General List',
313 'fw-mvc' => 'Zend Framework MVC List',
314 'fw-auth' => 'Zend Framwork Authentication and ACL List',
315 'fw-services' => 'Zend Framework Web Services List',
317 $lists = new Zend_Form_SubForm();
318 $lists->addElements(array(
319 new Zend_Form_Element_MultiCheckbox('subscriptions', array(
321 'Which lists would you like to subscribe to?',
322 'multiOptions' => $listOptions,
324 'filters' => array('StringTrim'),
325 'validators' => array(
328 array(array_keys($listOptions)))
333 // サブフォームをメインフォームにアタッチします
334 $this->addSubForms(array(
345 またサブフォームのデコレータではなにもしていないことに注意しましょう。
346 そのままでは、これらのサブフォームはフィールドセットとして表示されることになります。
347 つまり、処理をオーバーライドしてそれらを個別のサブフォームになるようにし、
348 さらに submit ボタンを追加して処理を進められるようにする必要があります。
349 submit ボタンには action プロパティと method プロパティも必要です。
350 では、これらの機能のとっかかりを先ほどのクラスに追加してみましょう。
353 <programlisting language="php"><![CDATA[
354 class My_Form_Registration extends Zend_Form
361 * @param string|Zend_Form_SubForm $spec
362 * @return Zend_Form_SubForm
364 public function prepareSubForm($spec)
366 if (is_string($spec)) {
367 $subForm = $this->{$spec};
368 } elseif ($spec instanceof Zend_Form_SubForm) {
371 throw new Exception('Invalid argument passed to ' .
372 __FUNCTION__ . '()');
374 $this->setSubFormDecorators($subForm)
375 ->addSubmitButton($subForm)
376 ->addSubFormActions($subForm);
381 * Form デコレータを各サブフォームに追加する
383 * @param Zend_Form_SubForm $subForm
384 * @return My_Form_Registration
386 public function setSubFormDecorators(Zend_Form_SubForm $subForm)
388 $subForm->setDecorators(array(
390 array('HtmlTag', array('tag' => 'dl',
391 'class' => 'zend_form')),
398 * submit ボタンを各サブフォームに追加する
400 * @param Zend_Form_SubForm $subForm
401 * @return My_Form_Registration
403 public function addSubmitButton(Zend_Form_SubForm $subForm)
405 $subForm->addElement(new Zend_Form_Element_Submit(
408 'label' => 'Save and continue',
417 * action と method をサブフォームに追加する
419 * @param Zend_Form_SubForm $subForm
420 * @return My_Form_Registration
422 public function addSubFormActions(Zend_Form_SubForm $subForm)
424 $subForm->setAction('/registration/process')
432 次に、アクションコントローラ用の仕組みを追加する必要があります。
433 さらにいくつか考えなければならないこともあります。
434 まず、フォームの入力内容をリクエスト間で持続させなければなりません。
435 次に、フォームの情報のうちどの部分が入力済みなのか、
436 そしてその部分に対応するサブフォームがどれなのか
437 といった情報を取得するロジックも必要です。今回は
438 <classname>Zend_Session_Namespace</classname> を使用してデータを持続させることにします。
439 そうすれば、二番目の問題に対応するのも簡単になるでしょう。
443 それではコントローラを作成していきましょう。
444 そして、フォームのインスタンスを取得するためのメソッドを追加します。
447 <programlisting language="php"><![CDATA[
448 class RegistrationController extends Zend_Controller_Action
452 public function getForm()
454 if (null === $this->_form) {
455 $this->_form = new My_Form_Registration();
463 それでは、どのフォームを表示するのかを決める機能を追加していきましょう。
464 基本的に、フォーム全体の入力内容の検証を終えるまでは
465 フォームの一部の表示を続けることになります。
466 さらに、普通はそれを決まった順序で表示することになるでしょう。
467 今回の場合は user、demog、そして最後に lists といった具合です。
468 どのデータが入力済みかを調べるには、セッションの名前空間を調べます。
469 各サブフォームに対応するキーが存在するかどうかを調べるというわけです。
472 <programlisting language="php"><![CDATA[
473 class RegistrationController extends Zend_Controller_Action
477 protected $_namespace = 'RegistrationController';
483 * @return Zend_Session_Namespace
485 public function getSessionNamespace()
487 if (null === $this->_session) {
489 new Zend_Session_Namespace($this->_namespace);
492 return $this->_session;
496 * すでにセッションに保存済みであるフォームの一覧を取得する
500 public function getStoredForms()
503 foreach ($this->getSessionNamespace() as $key => $value) {
511 * 使用できるすべてのサブフォームの一覧を取得する
515 public function getPotentialForms()
517 return array_keys($this->getForm()->getSubForms());
523 * @return false|Zend_Form_SubForm
525 public function getCurrentSubForm()
527 $request = $this->getRequest();
528 if (!$request->isPost()) {
532 foreach ($this->getPotentialForms() as $name) {
533 if ($data = $request->getPost($name, false)) {
534 if (is_array($data)) {
535 return $this->getForm()->getSubForm($name);
547 * @return Zend_Form_SubForm|false
549 public function getNextSubForm()
551 $storedForms = $this->getStoredForms();
552 $potentialForms = $this->getPotentialForms();
554 foreach ($potentialForms as $name) {
555 if (!in_array($name, $storedForms)) {
556 return $this->getForm()->getSubForm($name);
566 上のメソッドを使用すると、たとえば "<command>$subForm =
567 $this->getCurrentSubForm();</command>"
568 で現在のサブフォームを取得してそれを検証したり
569 "<command>$next = $this->getNextSubForm();</command>"
570 で次に表示するフォームを取得したりできます。
574 では、実際にサブフォームを処理したり表示したりする方法を考えてみましょう。
575 <methodname>getCurrentSubForm()</methodname> を使用すれば、
576 今送信されてきたデータがどのサブフォームのものなのかがわかります
577 (<constant>FALSE</constant> が返された場合は、まだ何も表示あるいは送信されていないことを表します)。
578 また、<methodname>getNextSubForm()</methodname>
579 を使用すれば次に表示すべきフォームを取得できます。
580 そして、フォームの <methodname>prepareSubForm()</methodname>
581 メソッドを使用すれば、フォームを表示するための準備を行えます。
585 フォームを送信したら、サブフォームのデータを検証し、
586 そしてフォーム全体の入力が完了したかどうかを調べることができます。
587 これらの作業を行うためには、さらにいくつかのメソッドを追加しなければなりません。
588 送信されたデータをセッションに追加するメソッドや、
589 フォーム全体の検証を行う際にセッションの全セグメントを検証するメソッドなどです。
592 <programlisting language="php"><![CDATA[
593 class RegistrationController extends Zend_Controller_Action
600 * @param Zend_Form_SubForm $subForm
604 public function subFormIsValid(Zend_Form_SubForm $subForm,
607 $name = $subForm->getName();
608 if ($subForm->isValid($data)) {
609 $this->getSessionNamespace()->$name = $subForm->getValues();
621 public function formIsValid()
624 foreach ($this->getSessionNamespace() as $key => $info) {
628 return $this->getForm()->isValid($data);
635 ではこのコントローラのアクションを作っていきましょう。
637 それからフォームを処理するための 'process'
641 <programlisting language="php"><![CDATA[
642 class RegistrationController extends Zend_Controller_Action
646 public function indexAction()
648 // 現在のページを再表示するか、"次の" (最初の)
650 if (!$form = $this->getCurrentSubForm()) {
651 $form = $this->getNextSubForm();
653 $this->view->form = $this->getForm()->prepareSubForm($form);
656 public function processAction()
658 if (!$form = $this->getCurrentSubForm()) {
659 return $this->_forward('index');
662 if (!$this->subFormIsValid($form,
663 $this->getRequest()->getPost())) {
664 $this->view->form = $this->getForm()->prepareSubForm($form);
665 return $this->render('index');
668 if (!$this->formIsValid()) {
669 $form = $this->getNextSubForm();
670 $this->view->form = $this->getForm()->prepareSubForm($form);
671 return $this->render('index');
676 $this->view->info = $this->getSessionNamespace();
677 $this->render('verification');
683 お気づきのとおり、実際にフォームを処理する部分のコードは比較的シンプルです。
684 注目すべき点は、どのサブフォームが送信されてきたのかを調べ、
685 何も送信されていない場合は先頭ページに飛ばしている部分です。
686 サブフォームが送信されてきた場合はそれを検証し、
687 問題がある場合は同じサブフォームを再表示します。
688 問題がない場合は、フォーム全体の入力が妥当か
689 (つまりすべての入力が終わっているか) を調べ、
690 問題がある場合は次のサブフォームを表示します。
691 最後に、セッションの中身を確認ページに表示します。
695 ビュースクリプトは非常にシンプルなものになります。
698 <programlisting language="php"><![CDATA[
699 <?php // registration/index.phtml ?>
701 <?php echo $this->form ?>
703 <?php // registration/verification.phtml ?>
704 <h2>登録ありがとうございます!</h2>
710 // 入力内容はセッション名前空間に格納されているので、
712 foreach ($this->info as $info):
713 foreach ($info as $form => $data): ?>
714 <h4><?php echo ucfirst($form) ?>:</h4>
716 <?php foreach ($data as $key => $value): ?>
717 <dt><?php echo ucfirst($key) ?></dt>
718 <?php if (is_array($value)):
719 foreach ($value as $label => $val): ?>
720 <dd><?php echo $val ?></dd>
723 <dd><?php echo $this->escape($value) ?></dd>
732 将来的に、Zend Framework には複数ページのフォームを
733 より簡単に作成するためのコンポーネントが用意される予定です。
735 セッションや各フォームの順序などの管理を抽象化したものとなります。
736 現時点では、複数ページのフォームをあなたのサイトで使用するには