[GENERIC] Zend_Translate:
[zend.git] / documentation / manual / ja / module_specs / Zend_Form-Advanced.xml
blobb0699efb287cbbfea2fd0db507937a690448da8b
1 <?xml version="1.0" encoding="UTF-8"?>
2 <!-- Reviewed: no -->
3 <!-- EN-Revision: 20774 -->
4 <sect1 id="zend.form.advanced">
5     <title>Zend_Form の高度な使用法</title>
7     <para>
8         <classname>Zend_Form</classname> にはさまざまな機能があり、
9         その多くは熟練者向けに用意されています。本章では、
10         それらの機能について例を交えて説明します。
11     </para>
13     <sect2 id="zend.form.advanced.arrayNotation">
14         <title>配列記法</title>
16         <para>
17             関連するフォーム要素について、要素名を配列形式にしてグループ化したいこともあるでしょう。
18             たとえば、配送先と請求先のふたつの住所を受け取りたい場合、
19             それぞれに同じ要素を使った上で配列でグループ化すれば、
20             結果を別々に受け取ることができます。
21             たとえば次のようなフォームを例に考えてみましょう。
22         </para>
24         <programlisting language="html"><![CDATA[
25 <form>
26     <fieldset>
27         <legend>配送先</legend>
28         <dl>
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>
43         </dl>
44     </fieldset>
46     <fieldset>
47         <legend>請求先</legend>
48         <dl>
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>
63         </dl>
64     </fieldset>
66     <dl>
67         <dt><label for="terms">I agree to the Terms of Service</label></dt>
68         <dd><input name="terms" type="checkbox" value="" /></dd>
70         <dt></dt>
71         <dd><input name="save" type="submit" value="Save" /></dd>
72     </dl>
73 </form>
74 ]]></programlisting>
76         <para>
77             この例では、請求先住所と配送先住所に同じフィールドを使用しているため、
78             一方が他方を上書きしてしまいます。
79             これを解決するには、配列記法を使用します。
80         </para>
82         <programlisting language="html"><![CDATA[
83 <form>
84     <fieldset>
85         <legend>配送先</legend>
86         <dl>
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>
106         </dl>
107     </fieldset>
109     <fieldset>
110         <legend>請求先</legend>
111         <dl>
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>
131         </dl>
132     </fieldset>
134     <dl>
135         <dt><label for="terms">I agree to the Terms of Service</label></dt>
136         <dd><input name="terms" type="checkbox" value="" /></dd>
138         <dt></dt>
139         <dd><input name="save" type="submit" value="Save" /></dd>
140     </dl>
141 </form>
142 ]]></programlisting>
144         <para>
145             上の例では、住所をそれぞれ個別に受け取ることができます。
146             このフォームを送信すると、受け取り側では 3 つの要素を取得できます。
147             'save' が送信ボタン、そしてふたつの配列 'shipping' と 'billing'
148             の中にはさまざまなキーとそれに対応する要素が含まれています。
149         </para>
151         <para>
152             <classname>Zend_Form</classname> は、この処理を
153             <link linkend="zend.form.forms.subforms">サブフォーム</link>
154             で自動化します。デフォルトで、
155             サブフォームのレンダリングには先ほどのような配列記法を使用します。
156             配列の名前はサブフォーム名からとられ、
157             配列のキーはサブフォーム内に含まれる要素となります。
158             サブフォームは、何段階でもネストさせることができます。
159             その場合も、ネストした配列形式でその構造を表します。
160             さらに、<classname>Zend_Form</classname>
161             のさまざまなバリデーション機能は、この配列構造をきちんと処理するようにできています。
162             サブフォームをどれだけ深くネストさせたとしても、
163             フォームの検証は正しく行ってくれます。
164             この機能を使うために特に何かしなければならないということはありません。
165             この機能はデフォルトで有効になっています。
166         </para>
168         <para>
169             さらに、条件付きで配列記法を有効にしたり
170             特定の配列を指定してそこに要素やコレクションを所属させたりといった機能もあります。
171         </para>
173         <itemizedlist>
174             <listitem>
175                 <para>
176                     <methodname>Zend_Form::setIsArray($flag)</methodname>:
177                     このフラグを <constant>TRUE</constant> にすると、フォーム全体を配列として扱うことができます。
178                     デフォルトでは、<methodname>setElementsBelongTo()</methodname>
179                     がコールされていない限りはフォーム名を配列の名前とします。
180                     フォームに名前が設定されていない場合や
181                     <methodname>setElementsBelongTo()</methodname> が設定されていない場合は、
182                     このフラグは無視されます (要素が属する配列の名前がないからです)。
183                 </para>
185                 <para>
186                     フォームが配列として扱われているかどうかを知りたい場合には
187                     <methodname>isArray()</methodname> アクセサを使用します。
188                 </para>
189             </listitem>
191             <listitem>
192                 <para>
193                     <methodname>Zend_Form::setElementsBelongTo($array)</methodname>:
194                     このメソッドを使用すると、フォームの全要素が属する
195                     配列の名前を指定できます。現在設定されている値を調べるには
196                     <methodname>getElementsBelongTo()</methodname> アクセサを使用します。
197                 </para>
198             </listitem>
199         </itemizedlist>
201         <para>
202             さらに、要素レベルでは、特定の要素を特定の配列に属させるために
203             <methodname>Zend_Form_Element::setBelongsTo()</methodname> メソッドを使うこともできます。
204             この値が何者なのか (明示的に設定されたものなのか
205             フォームを経由して暗黙的に設定されたものなのか)
206             を知るには <methodname>getBelongsTo()</methodname> アクセサを使用します。
207         </para>
208     </sect2>
210     <sect2 id="zend.form.advanced.multiPage">
211         <title>複数ページのフォーム</title>
213         <para>
214             現在、複数ページのフォームは
215             <classname>Zend_Form</classname> では公式にはサポートしていません。
216             しかし、それを実装するための機能の大半はサポートしており、
217             ほんの少し手を加えるだけでこの機能を実現できます。
218         </para>
220         <para>
221             複数ページのフォームを作成する鍵となるのが、
222             サブフォームの活用です。各ページに、ひとつのサブフォームだけを表示させるわけです。
223             こうすれば、それぞれのサブフォームの内容を各ページで検証し、
224             かつすべてのサブフォームの入力を終えるまでフォームの処理を行わないということができます。
225         </para>
227         <example id="zend.form.advanced.multiPage.registration">
228             <title>登録フォームの例</title>
230             <para>
231                 例として、登録フォームを考えてみましょう。
232                 まず最初のページでユーザ名とパスワードを入力してもらい、
233                 次のページではユーザのメタデータ
234                 (姓、名、住所など)、そして最後のページでは
235                 参加したいメーリングリストを選択するといったものです。
236             </para>
238             <para>
239                 まずはフォームを作成し、
240                 その中でサブフォームをいくつか定義します。
241             </para>
243             <programlisting language="php"><![CDATA[
244 class My_Form_Registration extends Zend_Form
246     public function init()
247     {
248         // ユーザサブフォーム (ユーザ名とパスワード) を作成します
249         $user = new Zend_Form_SubForm();
250         $user->addElements(array(
251             new Zend_Form_Element_Text('username', array(
252                 'required'   => true,
253                 'label'      => 'Username:',
254                 'filters'    => array('StringTrim', 'StringToLower'),
255                 'validators' => array(
256                     'Alnum',
257                     array('Regex',
258                           false,
259                           array('/^[a-z][a-z0-9]{2,}$/'))
260                 )
261             )),
263             new Zend_Form_Element_Password('password', array(
264                 'required'   => true,
265                 'label'      => 'Password:',
266                 'filters'    => array('StringTrim'),
267                 'validators' => array(
268                     'NotEmpty',
269                     array('StringLength', false, array(6))
270                 )
271             )),
272         ));
274         // 詳細サブフォーム (姓、名、住所) を作成します
275         $demog = new Zend_Form_SubForm();
276         $demog->addElements(array(
277             new Zend_Form_Element_Text('givenName', array(
278                 'required'   => true,
279                 'label'      => 'Given (First) Name:',
280                 'filters'    => array('StringTrim'),
281                 'validators' => array(
282                     array('Regex',
283                           false,
284                           array('/^[a-z][a-z0-9., \'-]{2,}$/i'))
285                 )
286             )),
288             new Zend_Form_Element_Text('familyName', array(
289                 'required'   => true,
290                 'label'      => 'Family (Last) Name:',
291                 'filters'    => array('StringTrim'),
292                 'validators' => array(
293                     array('Regex',
294                           false,
295                           array('/^[a-z][a-z0-9., \'-]{2,}$/i'))
296                 )
297             )),
299             new Zend_Form_Element_Text('location', array(
300                 'required'   => true,
301                 'label'      => 'Your Location:',
302                 'filters'    => array('StringTrim'),
303                 'validators' => array(
304                     array('StringLength', false, array(2))
305                 )
306             )),
307         ));
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',
316         );
317         $lists = new Zend_Form_SubForm();
318         $lists->addElements(array(
319             new Zend_Form_Element_MultiCheckbox('subscriptions', array(
320                 'label'        =>
321                     'Which lists would you like to subscribe to?',
322                 'multiOptions' => $listOptions,
323                 'required'     => true,
324                 'filters'      => array('StringTrim'),
325                 'validators'   => array(
326                     array('InArray',
327                           false,
328                           array(array_keys($listOptions)))
329                 )
330             )),
331         ));
333         // サブフォームをメインフォームにアタッチします
334         $this->addSubForms(array(
335             'user'  => $user,
336             'demog' => $demog,
337             'lists' => $lists
338         ));
339     }
341 ]]></programlisting>
343             <para>
344                 submit ボタンがないこと、
345                 またサブフォームのデコレータではなにもしていないことに注意しましょう。
346                 そのままでは、これらのサブフォームはフィールドセットとして表示されることになります。
347                 つまり、処理をオーバーライドしてそれらを個別のサブフォームになるようにし、
348                 さらに submit ボタンを追加して処理を進められるようにする必要があります。
349                 submit ボタンには action プロパティと method プロパティも必要です。
350                 では、これらの機能のとっかかりを先ほどのクラスに追加してみましょう。
351             </para>
353             <programlisting language="php"><![CDATA[
354 class My_Form_Registration extends Zend_Form
356     // ...
358     /**
359      * 表示用のサブフォームを準備する
360      *
361      * @param  string|Zend_Form_SubForm $spec
362      * @return Zend_Form_SubForm
363      */
364     public function prepareSubForm($spec)
365     {
366         if (is_string($spec)) {
367             $subForm = $this->{$spec};
368         } elseif ($spec instanceof Zend_Form_SubForm) {
369             $subForm = $spec;
370         } else {
371             throw new Exception('Invalid argument passed to ' .
372                                 __FUNCTION__ . '()');
373         }
374         $this->setSubFormDecorators($subForm)
375              ->addSubmitButton($subForm)
376              ->addSubFormActions($subForm);
377         return $subForm;
378     }
380     /**
381      * Form デコレータを各サブフォームに追加する
382      *
383      * @param  Zend_Form_SubForm $subForm
384      * @return My_Form_Registration
385      */
386     public function setSubFormDecorators(Zend_Form_SubForm $subForm)
387     {
388         $subForm->setDecorators(array(
389             'FormElements',
390             array('HtmlTag', array('tag' => 'dl',
391                                    'class' => 'zend_form')),
392             'Form',
393         ));
394         return $this;
395     }
397     /**
398      * submit ボタンを各サブフォームに追加する
399      *
400      * @param  Zend_Form_SubForm $subForm
401      * @return My_Form_Registration
402      */
403     public function addSubmitButton(Zend_Form_SubForm $subForm)
404     {
405         $subForm->addElement(new Zend_Form_Element_Submit(
406             'save',
407             array(
408                 'label'    => 'Save and continue',
409                 'required' => false,
410                 'ignore'   => true,
411             )
412         ));
413         return $this;
414     }
416     /**
417      * action と method をサブフォームに追加する
418      *
419      * @param  Zend_Form_SubForm $subForm
420      * @return My_Form_Registration
421      */
422     public function addSubFormActions(Zend_Form_SubForm $subForm)
423     {
424         $subForm->setAction('/registration/process')
425                 ->setMethod('post');
426         return $this;
427     }
429 ]]></programlisting>
431             <para>
432                 次に、アクションコントローラ用の仕組みを追加する必要があります。
433                 さらにいくつか考えなければならないこともあります。
434                 まず、フォームの入力内容をリクエスト間で持続させなければなりません。
435                 次に、フォームの情報のうちどの部分が入力済みなのか、
436                 そしてその部分に対応するサブフォームがどれなのか
437                 といった情報を取得するロジックも必要です。今回は
438                 <classname>Zend_Session_Namespace</classname> を使用してデータを持続させることにします。
439                 そうすれば、二番目の問題に対応するのも簡単になるでしょう。
440             </para>
442             <para>
443                 それではコントローラを作成していきましょう。
444                 そして、フォームのインスタンスを取得するためのメソッドを追加します。
445             </para>
447             <programlisting language="php"><![CDATA[
448 class RegistrationController extends Zend_Controller_Action
450     protected $_form;
452     public function getForm()
453     {
454         if (null === $this->_form) {
455             $this->_form = new My_Form_Registration();
456         }
457         return $this->_form;
458     }
460 ]]></programlisting>
462             <para>
463                 それでは、どのフォームを表示するのかを決める機能を追加していきましょう。
464                 基本的に、フォーム全体の入力内容の検証を終えるまでは
465                 フォームの一部の表示を続けることになります。
466                 さらに、普通はそれを決まった順序で表示することになるでしょう。
467                 今回の場合は user、demog、そして最後に lists といった具合です。
468                 どのデータが入力済みかを調べるには、セッションの名前空間を調べます。
469                 各サブフォームに対応するキーが存在するかどうかを調べるというわけです。
470             </para>
472             <programlisting language="php"><![CDATA[
473 class RegistrationController extends Zend_Controller_Action
475     // ...
477     protected $_namespace = 'RegistrationController';
478     protected $_session;
480     /**
481      * 使用するセッション名前空間を取得する
482      *
483      * @return Zend_Session_Namespace
484      */
485     public function getSessionNamespace()
486     {
487         if (null === $this->_session) {
488             $this->_session =
489                 new Zend_Session_Namespace($this->_namespace);
490         }
492         return $this->_session;
493     }
495     /**
496      * すでにセッションに保存済みであるフォームの一覧を取得する
497      *
498      * @return array
499      */
500     public function getStoredForms()
501     {
502         $stored = array();
503         foreach ($this->getSessionNamespace() as $key => $value) {
504             $stored[] = $key;
505         }
507         return $stored;
508     }
510     /**
511      * 使用できるすべてのサブフォームの一覧を取得する
512      *
513      * @return array
514      */
515     public function getPotentialForms()
516     {
517         return array_keys($this->getForm()->getSubForms());
518     }
520     /**
521      * 今どのサブフォームが送信されたのか?
522      *
523      * @return false|Zend_Form_SubForm
524      */
525     public function getCurrentSubForm()
526     {
527         $request = $this->getRequest();
528         if (!$request->isPost()) {
529             return false;
530         }
532         foreach ($this->getPotentialForms() as $name) {
533             if ($data = $request->getPost($name, false)) {
534                 if (is_array($data)) {
535                     return $this->getForm()->getSubForm($name);
536                     break;
537                 }
538             }
539         }
541         return false;
542     }
544     /**
545      * 次に表示するサブフォームを取得する
546      *
547      * @return Zend_Form_SubForm|false
548      */
549     public function getNextSubForm()
550     {
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);
557             }
558         }
560         return false;
561     }
563 ]]></programlisting>
565             <para>
566                 上のメソッドを使用すると、たとえば "<command>$subForm =
567                     $this-&gt;getCurrentSubForm();</command>"
568                 で現在のサブフォームを取得してそれを検証したり
569                 "<command>$next = $this-&gt;getNextSubForm();</command>"
570                 で次に表示するフォームを取得したりできます。
571             </para>
573             <para>
574                 では、実際にサブフォームを処理したり表示したりする方法を考えてみましょう。
575                 <methodname>getCurrentSubForm()</methodname> を使用すれば、
576                 今送信されてきたデータがどのサブフォームのものなのかがわかります
577                 (<constant>FALSE</constant> が返された場合は、まだ何も表示あるいは送信されていないことを表します)。
578                 また、<methodname>getNextSubForm()</methodname>
579                 を使用すれば次に表示すべきフォームを取得できます。
580                 そして、フォームの <methodname>prepareSubForm()</methodname>
581                 メソッドを使用すれば、フォームを表示するための準備を行えます。
582             </para>
584             <para>
585                 フォームを送信したら、サブフォームのデータを検証し、
586                 そしてフォーム全体の入力が完了したかどうかを調べることができます。
587                 これらの作業を行うためには、さらにいくつかのメソッドを追加しなければなりません。
588                 送信されたデータをセッションに追加するメソッドや、
589                 フォーム全体の検証を行う際にセッションの全セグメントを検証するメソッドなどです。
590             </para>
592             <programlisting language="php"><![CDATA[
593 class RegistrationController extends Zend_Controller_Action
595     // ...
597     /**
598      * サブフォームの入力は妥当か?
599      *
600      * @param  Zend_Form_SubForm $subForm
601      * @param  array $data
602      * @return bool
603      */
604     public function subFormIsValid(Zend_Form_SubForm $subForm,
605                                    array $data)
606     {
607         $name = $subForm->getName();
608         if ($subForm->isValid($data)) {
609             $this->getSessionNamespace()->$name = $subForm->getValues();
610             return true;
611         }
613         return false;
614     }
616     /**
617      * フォーム全体の入力は妥当か?
618      *
619      * @return bool
620      */
621     public function formIsValid()
622     {
623         $data = array();
624         foreach ($this->getSessionNamespace() as $key => $info) {
625             $data[$key] = $info;
626         }
628         return $this->getForm()->isValid($data);
629     }
631 ]]></programlisting>
633             <para>
634                 これで足場は固まりました。
635                 ではこのコントローラのアクションを作っていきましょう。
636                 まずこのフォームの最初のページ、
637                 それからフォームを処理するための 'process'
638                 アクションが必要となります。
639             </para>
641             <programlisting language="php"><![CDATA[
642 class RegistrationController extends Zend_Controller_Action
644     // ...
646     public function indexAction()
647     {
648         // 現在のページを再表示するか、"次の" (最初の)
649         // サブフォームを取得します
650         if (!$form = $this->getCurrentSubForm()) {
651             $form = $this->getNextSubForm();
652         }
653         $this->view->form = $this->getForm()->prepareSubForm($form);
654     }
656     public function processAction()
657     {
658         if (!$form = $this->getCurrentSubForm()) {
659             return $this->_forward('index');
660         }
662         if (!$this->subFormIsValid($form,
663                                    $this->getRequest()->getPost())) {
664             $this->view->form = $this->getForm()->prepareSubForm($form);
665             return $this->render('index');
666         }
668         if (!$this->formIsValid()) {
669             $form = $this->getNextSubForm();
670             $this->view->form = $this->getForm()->prepareSubForm($form);
671             return $this->render('index');
672         }
674         // フォームの入力が完了しました!
675         // 確認ページに情報を表示します
676         $this->view->info = $this->getSessionNamespace();
677         $this->render('verification');
678     }
680 ]]></programlisting>
682             <para>
683                 お気づきのとおり、実際にフォームを処理する部分のコードは比較的シンプルです。
684                 注目すべき点は、どのサブフォームが送信されてきたのかを調べ、
685                 何も送信されていない場合は先頭ページに飛ばしている部分です。
686                 サブフォームが送信されてきた場合はそれを検証し、
687                 問題がある場合は同じサブフォームを再表示します。
688                 問題がない場合は、フォーム全体の入力が妥当か
689                 (つまりすべての入力が終わっているか) を調べ、
690                 問題がある場合は次のサブフォームを表示します。
691                 最後に、セッションの中身を確認ページに表示します。
692             </para>
694             <para>
695                 ビュースクリプトは非常にシンプルなものになります。
696             </para>
698             <programlisting language="php"><![CDATA[
699 <?php // registration/index.phtml ?>
700 <h2>登録</h2>
701 <?php echo $this->form ?>
703 <?php // registration/verification.phtml ?>
704 <h2>登録ありがとうございます!</h2>
706     入力された情報は次のとおりです。
707 </p>
710 // 入力内容はセッション名前空間に格納されているので、
711 // このようにしなければなりません
712 foreach ($this->info as $info):
713     foreach ($info as $form => $data): ?>
714 <h4><?php echo ucfirst($form) ?>:</h4>
715 <dl>
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>
721         <?php endforeach;
722        else: ?>
723     <dd><?php echo $this->escape($value) ?></dd>
724     <?php endif;
725     endforeach; ?>
726 </dl>
727 <?php endforeach;
728 endforeach ?>
729 ]]></programlisting>
731             <para>
732                 将来的に、Zend Framework には複数ページのフォームを
733                 より簡単に作成するためのコンポーネントが用意される予定です。
734                 このコンポーネントは、
735                 セッションや各フォームの順序などの管理を抽象化したものとなります。
736                 現時点では、複数ページのフォームをあなたのサイトで使用するには
737                 上の例のようにするのが最も無難でしょう。
738             </para>
739         </example>
740     </sect2>
741 </sect1>
742 <!--
743 vim:se ts=4 sw=4 et: