Symfony Formの構造
ちょっと凝ったフォームを作ろうとして、公式ドキュメントだけでは細部がわからず、Symfony の FormやFormBuilder のソースを読んで調べようとすると、処理があちらこちらのクラスに飛んでいてすぐに迷子になってしまう。これは全体の構造を把握しておかないと毎度迷子の繰り返しになってダメだと思い、調査しておくことにした。
対象はSymfony 3.4。
全体の大まかな構造
- Form Factory
- Form Builderのオブジェクトを作成する。Symfony Formを使う入口となるサービス。
- Form Builder
- Formのオブジェクトを作成する。作成されるFormのツリーと相似形のツリーを構成する。
- Form
- フォームの入力欄 (input, select, ...) やボタンに対応するオブジェクト。
- フォーム自身もFormで表し、それをルートとするツリーを構成する。
- 役割としては、view data ⇔ norm data/model data の相互変換、Form Viewの作成など。
- Form View
- Form から抽出したデータで、Renderer に必要な情報を集めたもの。Form のオブジェクトと1対1対応し、Form と相似形のツリーを構成する。
- Renderer
- Form View のツリーを HTML に書き出す。
- twig 向けのファンクション (form, form_widget など) や Themes (form_div_layout.html.twigなど) が担当している部分。
- Form Type
- 入力欄の種類 (テキスト、プルダウン、...) を表す。
- FormやForm Builder の役割のうち、入力欄の種類によって異なる処理はForm Typeに委譲される。
※ なお、上記で使た用語はここでの説明の便宜上名付けたもので、必ずしもSymfonyでのクラス名やインタフェース名とは一致しない。
Symfony Formを使う側としては、作成したいフォームの構成に合わせて Formのツリーを構築すればよい。 その構築の手段として Form Builder, Form Factoryがある。Form ツリーが構築できれば、submit されたデータの受け取りや、RendererによるHTMLの生成が可能になる。
Formのツリー
Formのツリーの構造について
- ルートはフォーム自身
- リーフは入力欄 (input, select, ...) やボタン
- 複数の入力欄で構成される入力項目は子を持つ中間ノードとなる。
- 例: 郵便番号入力欄は、上3桁の入力欄と下4桁の入力欄の2つの子を持つ。
- 繰返しのある入力欄 (ex. CollectionType) は、繰り返される要素を子に持つ中間ノードとなる。
- 例: メールアドレスを任意個数入力できるフォームでは、1個のメールアドレス入力欄を子とし、その子を任意個数持つ中間ノードで構成される。
Formツリーは、次のようなクラス/インタフェースで構成される。Formツリーに限らないが、骨格を成すオブジェクトには必ず PHP のインタフェースが定義され、そのインタフェースを実装したクラスが定義される、という作りになっている。
インタフェース | 実装クラス (具象クラス) | |
---|---|---|
Formツリーのノード | FormInterface | Form, Button, SubmitButton |
Formツリーのノードの設定 | FormConfigInterface | FormBuilder, ButtonBuilder, SubmitButtonBuilder |
- 各ノードのクラスは基本的に入力欄の種類によらず Formクラス。
- ただし、ボタンだけ別のクラス。
- 各ノードの設定情報 ($builder->add(...) するときに渡す $options など) は FormConfigInterface実装クラスに格納。
- FormInterface実装クラスが持つ情報は、ツリー構成の情報とHTTPリクエストの処理などFormツリー作成後に作成される情報。
- Formツリー作成に当たって事前提供されるデータは FormConfigInterface 実装クラスが保持。
※ 以降、FormInterface 実装クラスを単に Form と呼ぶことにする。
Form に与えられた役割は、FormInterface のメソッドから読み取ると、
- ツリー構造の作成・取得
- setParent, add, remove, get, getRoot メソッドなど
- 「入力欄」として必要な振る舞いと、それに必要な情報・状態の保持
- Form View の作成
- createView メソッド
Form で実行する処理の内容は、入力欄の種類によって変わってくるため、入力欄の種類ごとに Form のサブクラスが用意されそうなものだが、そうはなっていない。その代わり、Form Type のクラスが入力欄ごとに用意されており、入力欄の種類によって異なる処理は Form Type に委譲する仕組みとなっている。
Form が子を持てるかどうかも入力欄の種類によって変わってくると考えられるが、これについてもリーフと非リーフのクラスを区別せず、どちらも Form クラスが使われる。ただし、compoundというフラグを (FormConfigInterfaceに)持っており、compound なものだけが子を持てる。
- 非compoundの場合、addなどツリーを操作するメソッドを呼び出すと例外が発生する。
- compound は、$builder->add(...)するときに渡す $options の設定項目の一つとなっている。
- compound のデフォルトは、Form Typeによって異なる。(ex. FormTypeやCollectionTypeならtrue, TextTypeならfalse)
Formツリーのリーフは画面上の入力欄に一対一対応するため、Webブラウザ側で動的に入力欄が追加削除される場合は、その後の構造に合わせてFormツリーの構造を変化させて対応する。その手段として Form Eventが利用でき、その実現例が CollectionType である。
入力欄の設定情報とFormConfigInterface
入力欄の設定情報は、Formのプロパティとしては保持せず、FormConfigInterface実装クラスに保持し、Formからは FormConfigInterface実装クラスを参照する構成となっている。
奇妙なことに、FormConfigInterfaceを実装したクラスとして FormConfigクラスは存在しない。FormConfigInterface実装クラスとして使われているのは FormBuilder クラスである。(この点については FormBuilder について説明するときに合わせて説明する)。
「入力欄の設定情報」は以下に大別される。
- いわゆる $options
- Form Type Reference の解説で Options としてリストアップされているもの
- アプリケーションまたは From Typeから、FormConfigBuilderInterface のメソッドを呼び出して設定されるもの
- viewTransformers
- modelTransformers
- Symfony Formのフレームワークによって設定されるもの
- type (Form Type (ResolvedFormTypeInterface))
- requestHandler (RequestHandlerInterface)
1の $options は array であり、構造上は任意のkey-valueペアが登録できるが、実際に指定できる設定情報は Form Type によって規定される。具体的には、FormTypeInterface::configureOptions()メソッドにて、OptionsResolverクラスを利用して定義される。OptionsResolverは、設定項目名とその型、デフォルト値を指定しておくと、値の検証や未設定時のデフォルト値適用を実施してくれる。
FormConfigInterfaceは上記1~3に対応するプロパティ(getter)を持つ。ただし、1については、array として持つと同時に、一部の設定情報はそれ専用のプロパティ(getter)が用意されている。たとえば required オプション については getRequired() メソッドがある。
また、FormConfigInterfaceはattributesプロパティを持つ。これも array で任意の key-value ペアが登録できる。$options との用途の違いは、アプリケーションからの設定情報に利用するのではなく、Formを作る前の段階で (Form Builderの処理中に) 内部的に Form に渡したい値が発生した場合に利用されるように見える。たとえば、CollectionType::buildForm()で利用されている。
Form Type
Form Typeは Form (≒入力欄) の種類 (テキスト、プルダウン、...) を表す。
- Symfony Formによってあらかじめ定義されている。
- 自作もできる。
Form Typeの役割は以下のとおりであり、本来FormやForm Builderが担うべき役割も含めて、種類によって異なる処理は Form Typeで引き受ける。そのため、種類を増やす場合はFormやForm Buiderのサブクラスは追加せずに Form Typeのサブクラスと Renderer の追加だけで済む。
- $options の仕様(項目名, デフォルト値など)を定義
- Form Builder を作成
- Form から Form Viewを作成
Form Type Reference では "Parent type" の欄に記載されているとおり、各 Form Type には「親」がある。親と子は is-a の関係にある。以下の図のとおり、ほとんどの Form Type が "FormType"の子孫。Buttonだけ別系統なのは、$options が異なるためと思われる。
この親子関係がそのままクラスの継承関係に反映されているかというと、そうではない。親子関係は、子のForm Type オブジェクトが、親のForm Typeオブジェクトを参照するという、オブジェクトツリーによって表される。そして、このツリーのノードは FormTypeではなく、ResolvedFormType という、ドキュメントでは見慣れないクラスが使われている。クラス図で表すと次のようになる。
オブジェクト図っぽく表すと、以下のようになり、ResolvedFormType によって Form Typeの親子関係が表されていることがわかりやすい。
親子関係をクラス継承ではなくオブジェクトの参照で表しているのは、Form Type Extension によって、既存のプログラムを変更することなく機能を拡張できるようにし、また、それが親子関係の末端だけでなく中間のForm Typeにも適用できるようにするため、と考えられる。
ResolvedFormType は次のように動作することで、親からの継承と、後付けの拡張の両方を実現する。たとえば、buildView メソッドであれば以下の順にメソッドを呼び出す。後で呼ばれたほうは、先に呼ばれたメソッドの出力を加工でき、それによって buildView の出力を変更できる。
- parent の buildView
- innerType (FormTypeInterface) の buildView
- typeExtensions (FormTypeExtensionInterface) の buildView
Form や Form Builder から参照される Form Type は、 FormTypeIntereface ではなく ResolvedFormTypeInterface である。
ResolvedFormType のインスタンスは、いわば、Form、Form Builder の「クラス」定義に相当する。Form、Form Builder のインスタンスが個々の入力欄それぞれに存在するのに対し、 ResolvedFormType や FormType のインスタンスは、同じ種類の入力欄に共通に1個だけ存在する。個々の入力欄の状態や設定情報は ResolvedFormType や FormType には持っていない。そのような情報は Form や FormConfigInterface のほうで持っている。
続き
Form Builder と Form Viewについてはまた後日...。
Horizontal Form の記述例メモ
bootstrap 3 の Horizontal Form。labelとinputを並べるだけのパターンから外れるといつも記述方法がわからなくなるため、しばしば遭遇するレイアウトについてHTML記述例をメモ。
レイアウト例
ソース
Symfony 2.8とPHP-DIのAuto Wiring
この記事は Symfony Advent Calendar 2015 の16日目です。昨日は@Issei_Mさんの「最近のSymfony Standard Editionのディレクトリ構成」でした。
内製フレームワークからの移行先を探してSymfonyを調査中です。その過程でDIのAuto Wiringの使い方を確認したので、ここにまとめます。
概要
Symfony 2.8ではサービスのAuto Wiringが可能になりました (New in Symfony 2.8: Service Auto Wiring (Symfony Blog))。これまでは、services.ymlにサービスの名前とクラス名の定義、依存先のサービスも(たとえばコンストラクタの引数として)指定が必要でした。Auto Wiring有効時は、コンストラクタのタイプヒンティングを利用して、これらの定義が簡略化できるようになっています。
また、Symfonyとは別のフレームワークになりますが、PHP向けのDIコンテナのひとつにPHP-DI があり、これもAuto Wiringをサポートしています。また、アノテーションを利用してプロパティに直接注入できる機能も備え、注入だけのためにコンストラクタを作成する必要がありません。Symfonyとの連携機能も用意されており、状況によっては、Symfonyのサービスコンテナよりさらに記述を簡略化できそうです。
この記事では、以下2点の基本的な使い方をご紹介します。
試した環境は次のとおりです。
Symfony 2.8 の AutoWiring
たとえば、サービスAにサービスBを注入したいとします。従来は、A, Bともにservices.ymlに定義が必要でした。
Auto Wiringを有効にすると、サービスAのargumentsの指定は不要になり、サービスBの定義全体も不要となります。ServiceAクラスのコンストラクタのタイプヒンティングで指定されたクラスが自動的に注入されます。
実際に簡単なコントローラを作成して試してみます。
プロジェクト作成
組み込みサーバの起動確認のため、ブラウザから http://localhost:8000/app_dev.php/ にアクセスしてWelcome画面の表示を確認します。
コントローラとサービスの作成
例として、/sample/hello/{名前} にアクセスすると「こんにちは{名前}さん」というメッセージが表示されるようにします。コントローラを次のように作成します。
- 名前を受け取って「こんにちは{名前}さん」というメッセージを構築するGreetingServiceを作成。
- コントローラはGreetingServiceを呼び出してメッセージを作成。
- コントローラは、サービスとして作成 (How to Define Controllers as Services (The Symfony CookBook)にしたがって作成)
先の例に当てはめると、service_Aがコントローラ、service_BがGreetingServiceにあたります。GreetingService、コントローラ、services.ymlの定義は次のようにしました。
これで http://localhost:8000/sample/hello/JohnSmith にアクセスすると「こんにちは、JohnSmith さん。」と表示されます。
Auto Wiringを利用する
次に、SampleControllerに対してAuto Wiringを有効にしてみます。有効にするには、autowiring: true を指定します。GreetingServiceについてのサービス定義は不要になるため削除してしまいます。services.ymlは次のとおりとなります。
SampleControllerがGreetingServiceを必要としていることはコンストラクタのタイプヒンティングに明記されており、services.ymlには、argumentの指定と、GreetingServiceのサービスとしての定義は不要になりました。SampleControllerクラスとGreetingServiceクラスには変更ありません。
なお、Auto Wiringが可能なのはタイプヒンティングに具象クラスが指定されている場合に限られるようです。サービスのinterfaceをclass (実装)と分けて定義している場合は、コンストラクタのタイプヒンティングにはinterface名を指定するかと思いますが、この場合には以下のとおりエラーになります。実装クラスを特定できないためでしょう。
Unable to autowire argument of type "AppBundle\GreetingServiceInterface" for the service "app.controller.hello".
依存のネストがある場合
さらに、GreetingServiceが別のサービスを利用するように拡張してみます。氏名を大文字化するCapitalizationServiceを追加し、GreetingServiceから利用するようにします。この場合でも、新しいサービスCapitalizationServiceをservices.ymlに記述する必要はなく、クラスの追加・修正だけで済みます。
SymfonyでのPHP-DIの利用
ここまで作成した処理を、PHP-DIを利用したものに書き換えてみます。
PHP-DIのSymfonyでの利用方法は、PHP-DIのサイトに説明があります。
PHP-DIの導入
まず、PHP-DIをcomposerでインストールします。
なお、私の環境では、インストール中のキャッシュクリアの処理でファイル削除失敗のエラーになってしまったため、app/cache/dev, app/cache/dev_oldを削除してcomposerを実行しなおしました。(php app/console cache:clear でも同様にエラーになります・・・なんとかならないものでしょうか・・・)
インストール後、app/AppKernel.phpに以下の処理を追加する必要があります。(バンドルとして追加されるわけではないようです。)
PHP-DIは次のような仕組みでSymfonyのサービスコンテナと共存するようです。
- Symfonyのサービスコンテナを、php-di/symfony2-bridgeが提供するもの(DI\Bridge\Symfony\SymfonyContainerBridge)にすり替え。
- php-di/symfony2-bridgeのサービスコンテナは、Symfonyのサービスコンテナクラスを継承しており、Symfonyのサービスコンテナと同様に動作。
- ただし、取得しようとしたサービスが見つからない場合に、PHP-DIに委譲。
したがって、PHP-DI導入後も先のプログラムソースのままで動作しますが、Symfonyのサービスコンテナの処理が働いている結果であり、PHP-DIには委譲されていないはずです。
PHP-DI経由でサービスを利用
PHP-DIは、明示的に定義のない場合は、オブジェクトの名前(サービス名)を、そのままクラス名として解釈してオブジェクト(サービス)を取得しようとします。したがって、コントローラクラスの@Routeのservice=にクラス名を指定できるようになります。
このとき、services.ymlにはSampleControllerの定義はもはや必要ありません。
SampleControllerの依存先(GreetingService)についてもSymfony 2.8と同様にタイプヒンティングからクラスが特定されコンストラクタ経由で注入されますので、やはりサービスの定義は不要です。
Symfonyが管理するサービスを利用
PHP-DIが管理するサービスに対して、Symfonyが管理するサービスを注入できます。
How to Define Controllers as Services (The Symfony CookBook によれば、サービスとして作成したコントローラからテンプレートを使うには、"templating" サービス(EngineInterfaceインタフェースの実装クラス)の注入が必要です。これをPHP-DIで管理されたコントローラに注入する方法のひとつは、インタフェースとオブジェクトの対応関係を明示的にPHP-DIに指定してやることです。この対応関係は、PHP-DI導入時にAppKernelに追加したPHP-DIの初期化処理に追加します。
\DI\get('templating') は、「コンテナから名前 templating で取得したオブジェクト」を表します。ここでのオブジェクト取得もまた、Symfony管理→PHP-DI管理の順に探索されるものと思われます。
この指定を追加した上で、SampleControllerのコンストラクタにEngineInterfaceを追加すれば、"templating"サービスが注入されます。
プロパティへの注入
PHP-DIでは@Injectを指定したプロパティにサービスを注入できます。この場合は、コンストラクタのタイプヒンティングの代わりに、プロパティの@varでクラスを指定します。
ただし、アノテーションはデフォルトで無効となっており、有効にするにはPHP-DI初期化時に指定する必要があります。
なお、Symfonyのアノテーションと異なり、@Inject の名前空間は考慮されていないようで、Inject の use を記述する必要がありません。
@Injectを使ってSymfonyが管理するサービスを利用
この@Injectを使ってSymfony管理のサービスを注入することもできます。@Injectの引数にサービス名を指定します。
この場合は、EngineInterfaceと"templating"サービスの対応関係を定義する必要はありません。
まとめ
- Symfony 2.8のAuto Wiringを使うと、サービスに別のサービスを注入する場合に後者のサービスの定義は省略でき、記述が簡素化されます。
- また、PHP-DIをSymfonyと連携させた場合は、
明日は tarokamikaze さんです。よろしくお願いします。
AndroidのSQLiteDatabaseを複数のスレッドで利用する
AndroidでSQLiteを使うにあたり、複数のスレッドからDBにアクセスする必要がある場合、SQLiteOpenHelperとSQLiteDatabaseのインスタンスをどう扱うべきか。
Androidの次のドキュメントからはマルチスレッドな場合の使い方がはっきりしなかった。
知りたかったのは、
- SQLiteOpenHelper / SQLiteDatabase はスレッドセーフか?
- SQLiteOpenHelper / SQLiteDatabase のインスタンスは複数作ってよい?closeはいつすべき?ライフサイクルがどうあるべき?
対象はAndroid 4.2.2。
今のところ次のとおりだと理解した。
-
SQLiteOpenHelperはスレッドセーフ。1データベースにつき1インスタンスとすべき。
-
SQLiteDatabaseもスレッドセーフ。1データベースにつき(closeしていないインスタンスは)1インスタンスとすべき。
-
SQLiteDatabaseをcloseするのは、全スレッドでそのインスタンスの利用を終えた後。
SQLiteOpenHelperは、Webを検索するとシングルトンにすべきという記事が多く見られる。テーブルの作成などを処理する onCreate, onUpgrade が並列実行されるのは不都合だが、SQLiteOpenHelperのソースを見るとsynchronizedで排他制御されており、同じインスタンスをスレッド間で共有しておけばonCreateなどが並列実行される心配がない。
SQLiteDatabaseは、SQLiteOpenHelper#getWritableDatabase(), #getReadableDatabase()で取得するが、SQLiteOpenHelperは一度作成したインスタンスをキャッシュして、SQLiteDatabaseがcloseされない限りは同じインスタンスを返す(APIリファレンスにも説明あり)ため、SQLiteOpenHelperをシングルトンとした場合はSQLiteDatabaseのインスタンスも1データベースにつき1個となる。
となるとSQLiteOpenHelperはスレッドセーフであってほしいが、ソースを見てもぱっと見は、なにやら複雑でいまひとつ確信が持てない。これについてはSQLiteSessionのjavadocに説明があった。このクラスは内部クラス?であるらしく、APIリファレンスには掲載されていない。ソースは以下を参照した。
この説明によれば
- SQLiteDatabaseは、ThreadLocalを使い、スレッドごとにSQLiteSessionのインスタンスを1個持つ。
- SQLiteSessionは、1トランザクションを実行するたびに、コネクションをプールから取得、返却。
- 1スレッドが同時に使うコネクションは高々1個となるように制御される。
というように、複数スレッドで共有して使うことが前提の仕組みとなっている。ソースを見ると、コネクションプールもSQLiteDatabase 1インスタンスに1個となっており、SQLiteDatabaseをスレッド間で共有しないとプールが効果を発揮しない。
SQLiteDatabase#close()を呼ぶべきタイミングについては、Webを検索すると見解が割れている。
- closeは呼ばない(finalizeに任せる)
- closeをスレッドごとに呼べ(あるいは、aqcuireReference()を呼んだ上で)
ソースを見たところ、あるスレッドでSQLiteDatabase#close()を呼ぶと、それを共有別スレッドでは、insertやqueryを呼び出した時点で、close済みという理由で例外が発生する。closeを呼ぶとすれば、SQLiteDatabaseを共有する全スレッドで利用が終了した後とする必要がある。closeの処理が、ThreadLocalな何らかのオブジェクトに転送されるような処理にもなっていなかったため、スレッドごとにcloseを呼ぶ必要はない。
逆にcloseを呼ばない場合のリークの心配については、SQLiteDatabaseのインスタンスが1つに保たれていること、SQLiteSessionがThreadLocalで管理されているためthreadが無くなればガーベージコレクションの対象となること、コネクションはSQLiteDatabaseのインスタンスごとにプールで管理されていることから、アプリ実行中に延々とリソースを食い尽くす心配はないように思われる。不安がある場合は、適当な時期に、どのスレッドもSQLiteDatabaseを使っていないタイミングを見計らってcloseすることになるだろう。close後に再度アクセスが必要になっても、getWritableDatabaseなどを呼び出せば利用可能なSQLiteDatabaseのインスタンスが新たに得られる。
なお、closeをfinalizeに任せると警告がログに出るとの記事もあったが、その件は未調査。
本件がややこしく感じてられ、また、closeの呼び出しタイミングで諸説生じてしまっているのは、SQLiteDatabaseがコネクションを表しているとの誤解が原因ではなかろうか。このAPIがコネクションを隠蔽しており、insertやqueryなどのメソッドがSQLiteDatabaseに用意されているために、SQLiteDatabase=コネクションという想像をしてしまっていた。SQLiteDatabaseを字面どおりデータベースと捕らえれば、それをスレッド間で共有し、すべてが使用を終えてからcloseするというのはすんなり受け入れられる。
JJUG CCC 2015 Spring メモ
Timetable / JJUG CCC 2015 Spring(4月11日開催) | 日本Javaユーザーグループ
覚えておきたいところ自分用メモ。
Javaにおけるnull。これまでとこれから (太一氏)
Javaにおけるnull。これまでとこれから - Google スライド
各言語のnullっぽいもの
- SQL
- 3値論理である(true, false, NULL)
- NULLは未知、または、適用不能、をあらわす。値ではない!
- SQL
言語仕様上nullが許されない場所
- Integer i; で i の unboxing
- for (String foo: bar) のbar
- synchronized(x) の x
- throw ex のex
何もないこと、をどうあらわすか
"空"のオブジェクト
- 空文字列
- 空の配列
- 空のListなど。Collections#emptyList()などで得られる。
型のあるnull
- NullObjectパターン。A型に対し、abstract Aと、そのサブクラスRealA、NullA を作る。
- ソースが増えるのがデメリット。
- NullObjectパターン。A型に対し、abstract Aと、そのサブクラスRealA、NullA を作る。
Optional
有効でない値、を受け取ったとき/受け取りうるときどうするか
nullを早期にガード
The Checker Framework にチェックさせる
※ 自分で調べてみたらこんなもの
- nullを考慮して処理
大規模な負荷でもドキドキしない為のJava EE (nagaseyasuhito氏)
動的に追加・削除できる入力欄をJSFで作成する
同じ内容を2個以上入力できるフォームで、ボタンクリックにより入力欄の追加や削除をできるようにしたい。Googleで検索してみるとui:repeatを使った解が見つかるが、実際試してみると、場合によっては不自然な挙動になり、実際に使うのには具合が悪かった。色々試した結果、PrimeFacesのp:dataListを使って実現できた。
確認環境
作成する画面例
- 氏名とe-mailアドレスをペアで入力する
- このペアの単位で、入力欄を追加・削除できる
Backing Bean
- 氏名, e-mailをプロパティとして持つMemberクラスを定義し
- Backing Beanには、List<Member> を持たせる。
Memberクラス
deleteについては後述。
Backing Bean (RegisterPageクラス)
プロパティmembersがList<Member>となっている。
初期表示時に1個だけ入力欄が表示されるように、initメソッドでMemberを1個だけmembersに追加。
actionのメソッドは後述。
Facelets
p:dataListのコンテントが、氏名・e-mailの入力欄ペアとなっており、これがp:dataListによって繰り返される。
p:dataListはPrimeFacesが提供するタグで、列数1に限定したh:dataTableのようなもの。h:dataTableはHTMLのtable/tr/tdタグを出力するが、p:dataListはul/li、ol/li、dl/ddを出力する。ただし、type="none"を指定することでul/liなどは出力されなくなるため、自分で(ほぼ)自由にHTMLを組み立てられ、ui:repeatに近い使い方もできる。
valueアトリビュート、varアトリビュートはui:repeatと同じで、それぞれ、参照するリストと、リストの要素を代入する変数名を指定する。#{registerPage.members} でList<Member>を参照し、その要素(この例ではMemberクラス)が member に代入されてp:dataListのコンテントが処理される。
p:dataListのコンテントでは、Memberクラスのname, emailプロパティそれぞれをh:inputTextに結びつけて入力できるようにしている。
以上で、List<Member>の要素数と同じだけ入力欄が表示されるようになる。
入力欄の追加
「入力欄を追加する」ボタンをクリックすればRegisterPage#addMember()メソッドによりList<Member>に要素が追加され、p:dataListで表示される入力欄も1個増える。
入力欄追加のh:commandButtonにはimmediate="true"を指定している。これはvalidationをスキップするためだ。Memberクラスのnameプロパティにはvalidationのルール(@NotNull @Size(min = 1, max = 10))を指定してあるが、「入力欄を追加」時にはvalidationをせずにアクションを実行したい。immediate="true"を指定することでvalidationをする前にアクションを呼出せる。
入力欄の削除
追加の逆で、List<Member>から要素を削除すれば良いように思われるし、「削除」ボタンにimmediate="true"を付けない場合はそれでうまく行った。しかし、immediate="true"を付けると、どの「削除」を押しても、最後の入力欄が削除されるという動作になった。
この問題を回避するために、List<Member>から要素を削除するのはあきらめ、代わりにMemberにdeleteプロパティを持たせ、これがtrueなら画面には表示しない、という方法を取った。delete=trueなら表示されないようにするため、p:dataListのコンテントは<ui:fragment rendered="#{not member.deleted}"> で囲んだ。
実用にあたって
- p:dataListは、内容全体を2つのdivタグで囲み、borderを表示する。このborderを消したい場合は、出力されたHTMLを見ながら、適切にCSSクラスとスタイルを追加する。上記の例では、p:dataList の styleClass="repeated-form" と、h:headタグ内のスタイル定義が該当する。divタグ自体は消せない。全体を<ul>などで囲みたい場合にはdivが間に入るとHTML文法上正しくないので困る。この場合はp:dataListにulやolを出力させるようにtype= を設定して回避する。<table>, <tr>などで囲みたい場合も困るが、この場合はtableを出力するp:dataTableに置き換えると良いかもしれない(未検証)。なお、h:dataTableはimmediate="true"指定時に入力値が維持されない問題がありNG。
- また、PrimeFacesはその表示にjQuery UI由来のスタイルを適用するため、PrimeFacesベースで画面を作っていない場合はp:dataListの中だけフォントサイズやボタンのスタイルが変わってしまう。これは、冒頭に示した表示例でも顕著だ。これに対処する正式な方法は、jQuery UIの「テーマ」を作成し、PrimeFacesのマニュアルに従い、組み込むことであるが、一筋縄ではいかない。ここもHTMLを観察しながらCSSクラスとスタイルでがんばるのが適当かもしれない。
- 追加・削除ボタンに <f:ajax ... /> を追加してAJAXによる部分再描画で画面を更新するようにしても、問題なく動作する。f:ajaxのrender アトリビュート(再描画対象)には、p:dataListのidか、さらにその外側のものを指定する。
- 氏名・email欄のvalueは、上記の例では #{member.name} などとしているが、代わりに、p:dataListのvarStatusと組み合わせて、#{registerPage.members[memberStatus.index].name} というように指定しても、理屈上は問題なさそうだし、Googleで検索して見つかるui:repeatの例では、むしろ後者しか動作しないと説明しているものもある。しかしp:dataListの場合は、後者は動作しなかった。NullPointerExceptionが発生したりする。p:dataListのvarStatusの動作が不完全のような印象を受けた。
- List<Member>から要素を削除すると画面に正しく反映されない現象は、h:inputText(UIInput)にsubmitted valueが残っている場合に発生するのではないかという気がする。submitted valueが残っていれば、Backing Beanの値ではなく、残っている submitted valueが表示される。h:commandButtonにimmediate="true"を指定すれば、Update Model Valuesフェーズはスキップされるからsubmitted valueが残ったままになる。submitted valueをクリアする手段があれば解決するかもしれない。ただし、List<Member>の、目的のn番目に対応するsubmitted valueだけピンポイントで消す必要がある。
- 今回の例の追加ボタンは、List<Member>の最後に要素を追加した。要件によっては途中に挿入したいケースもあるかもしれない。試していないが、削除が思ったような動作にならかったのと同じ理由で、途中へ挿入した場合も正しく表示できない気がする。
Mavenで一部のテストの実行/除外を切り替える
やりたいこと
- 一部のテストは、普段(mvn test で実行したとき)は実行されないようにする。
- ただし、mvn の引数で何かを指定すれば実行できるようにする。
- また、そのテストは、NetBeansから「ファイルのテスト」で単独実行されるようにする。
使用した環境
JUnitの@Categoryを使ってテストをグループ分け
「一部のテスト」には、JUnitの@Categoryでカテゴリを指定することで、他のテストと区別する。
Maven側では、特定のカテゴリのみテストを実行したり、テスト対象から除外したりできる。
カテゴリを指定するためには、カテゴリを表すマーカー用のクラスかインタフェースを作り、それをテストクラスに@Categoryを使って指定する。
マーカー用のインタフェースの例。インタフェース名は何でもよい。 (src/test/java 配下に置く)
package sample.maven.test; public interface ExtraTest { }
@Categoryを指定したテストクラスの例
package sample.maven; import org.junit.Test; import static org.junit.Assert.*; import org.junit.experimental.categories.Category; import sample.maven.test.ExtraTest; @Category(ExtraTest.class) public class GreeterSlowTest { @Test public void testSlowSayHello() { String actual = new Greeter().sayHello("Suzuki"); String expected = "Hello Suzuki!"; assertEquals(expected, actual); } }
普段のテストから指定のカテゴリを除外する
maven-surefire-plugin プラグインを設定して、指定のカテゴリをテスト対象から除外する。
pom.xmlにmaven-surefire-pluginの設定がなければ、暗黙の設定がどうなっているかを、mvn help:effective-pom で確認できる。mvn help:effective-pom を実行すると、pom.xmlで記述していないものも含め、現在有効になっている設定が表示される。試した環境では、maven-surefire-plugin の暗黙の設定は次のようになっていた。
<plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.10</version> <executions> <execution> <id>default-test</id> <phase>test</phase> <goals> <goal>test</goal> </goals> </execution> </executions> </plugin>
これを自分のpom.xmlに貼り付け、pom.xmlは次のようにした。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>sample</groupId> <artifactId>MavenSample</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>MavenSample</name> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <!-- 4.8以上が必要 --> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.16</version> <!-- 2.13以上? が必要 --> <executions> <execution> <id>default-test</id> <phase>test</phase> <goals> <goal>test</goal> </goals> <configuration> <excludedGroups>sample.maven.test.ExtraTest</excludedGroups> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
ポイントは、
- surefireの設定にexcludedGroupsを追加して、除外したいカテゴリのマーカー用クラス/インタフェースを指定。
また、
以上の設定で、 mvn test を実行したときには @Category(ExtraTest.class)の付いたテストは対象から除外される。
指定のテストだけ実行するための設定
surefireプラグインは、パラメータgroupsにカテゴリのマーカー用クラス/インタフェースを指定すると、そのカテゴリだけを対象にテストを実行する。これを利用すれば所望の設定ができる。
一方、surefireプラグインは、上記の設定により、ExtraTestは除外するよう設定済みである。しかし、
executionタグを追加し既存とは異なるidを指定することにより、別の設定のsurefireプラグインを追加することが可能となる。
<plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.16</version> <executions> <execution> <id>default-test</id> <phase>test</phase> <goals> <goal>test</goal> </goals> <configuration> <excludedGroups>sample.maven.test.ExtraTest</excludedGroups> </configuration> </execution> <!-- ここから追加 --> <execution> <id>extra-test</id> <phase>test</phase> <goals> <goal>test</goal> </goals> <configuration> <groups>sample.maven.test.ExtraTest</groups> </configuration> </execution> </executions> </plugin>
ここでは、extra-testというidでsurefireの設定を追加した。
なお、以下の指定は
<phase>test</phase> <goals> <goal>test</goal> </goals>
次のような意味になる。
surefireプラグインが提供しているゴールは"test" 1個だけであり、そのゴールはJUnitなどを使ってテストを実行する。つまり、mvn test と実行すれば、extra-testと名づけた設定でsurefireプラグインによるテストが実行される。
この設定のままだと mvn test したときには、デフォルト(id: default-test)と、追加した id: extra-test の両方が実行されてしまう。extra-testのほうは普段は実行せず、明示的に指定されたときだけ実行されるようにしたい。これは、surefireプラグインのパラメータ skipTests と、mavenのプロパティを利用して実現できる。
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <skipExtraTest>true</skipExtraTest> <!-- これを追加 --> </properties> <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.16</version> <executions> <execution> <id>default-test</id> <phase>test</phase> <goals> <goal>test</goal> </goals> <configuration> <excludedGroups>sample.maven.test.ExtraTest</excludedGroups> </configuration> </execution> <execution> <id>extra-test</id> <phase>test</phase> <goals> <goal>test</goal> </goals> <configuration> <skipTests>${skipExtraTest}</skipTests> <!-- これを追加 --> <groups>sample.maven.test.ExtraTest</groups> </configuration> </execution> </executions> </plugin> </plugins> </build>
surefireプラグインのskipTestsパラメータにtrueを指定すると実行はスキップされる。 skipTestsの値は、独自に追加したskipExtraTestプロパティで与えるようにして、skipExtraTestプロパティのデフォルト値はtrueにしておく(propertiesタグに追加した設定により指定している)。すなわち、デフォルトではスキップされる。
extra-testを実行したい場合は、次のようにskipExtraTestをfalseにして実行すればよい。
mvn -DskipExtraTest=false test
NetBeansからの実行
テストプログラム作成中は「ファイルをテスト」「ファイルのテストをデバッグ」を使って、作成中のテストクラスだけ実行させるのが便利だが、この場合は、@Categoryの有無にかかわらず指定のテストクラスが実行される。
どうやらこのケースの場合は、ここまでで定義したid: default-test, id: extra-testのどちらの設定も適用されていないようだ。「ファイルをテスト」の場合、NetBeansは、mvn test ではなく mvn surefire:test のように、直接ゴール(surefire:test)を指定して実行している。そしてこの場合適用される設定は id: default-test ではなくid: default-cli となるようだ。このことは、mvn surefire:test を実行したときの次の表示から推測できる。
--- maven-surefire-plugin:2.16:test (default-cli) @ MavenSample ---
なお、実行>プロジェクトをテスト の場合は、mvn test で実行されるため、id: extra-testはスキップされる。
備考
プラグインの設定記述例としてよく見かけるのは、次のようにexecutionsタグを使わずにconfigurationを書いている例だが、この場合は、暗黙に定義されているものも含め、すべてのidの設定にこの設定が追加されるようだ。
<plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.16</version> <configuration> <excludedGroups>sample.maven.test.ExtraTest</excludedGroups> </configuration> </plugin>
このため、上記の設定であれば、NetBeansから「ファイルをテスト」を実行した場合にもExtraTestは除外される。つまり、ExtraTestをつけたテストクラスは「ファイルをテスト」では実行できなくなってしまう。