プログラミングメモ

ソフトウェア開発に関する技術メモ。

Symfony Formの構造

ちょっと凝ったフォームを作ろうとして、公式ドキュメントだけでは細部がわからず、Symfony の FormやFormBuilder のソースを読んで調べようとすると、処理があちらこちらのクラスに飛んでいてすぐに迷子になってしまう。これは全体の構造を把握しておかないと毎度迷子の繰り返しになってダメだと思い、調査しておくことにした。

対象はSymfony 3.4。

全体の大まかな構造

f:id:teematsu:20190105134057p:plain

  • 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 メソッドなど
  • 「入力欄」として必要な振る舞いと、それに必要な情報・状態の保持
    • model data, norm data, view data
      • setData, submit, handleRequest, getViewData, isEmpty, getTransformationFailure, isSynchronized メソッドなど
    • 名前 (name)
    • required と disabled
    • 背後のPHPオブジェクトとのマッピング (propertyPath)
    • 検証エラー (errors)
    • 入力欄の設定 (config (FormConfigInterface))
  • 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 について説明するときに合わせて説明する)。

「入力欄の設定情報」は以下に大別される。

  1. いわゆる $options
  2. アプリケーションまたは From Typeから、FormConfigBuilderInterface のメソッドを呼び出して設定されるもの
    • viewTransformers
    • modelTransformers
  3. 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 (≒入力欄) の種類 (テキスト、プルダウン、...) を表す。

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 が異なるためと思われる。

f:id:teematsu:20190105134100p:plain:w180

この親子関係がそのままクラスの継承関係に反映されているかというと、そうではない。親子関係は、子のForm Type オブジェクトが、親のForm Typeオブジェクトを参照するという、オブジェクトツリーによって表される。そして、このツリーのノードは FormTypeではなく、ResolvedFormType という、ドキュメントでは見慣れないクラスが使われている。クラス図で表すと次のようになる。

f:id:teematsu:20190105134102p:plain:w600

オブジェクト図っぽく表すと、以下のようになり、ResolvedFormType によって Form Typeの親子関係が表されていることがわかりやすい。

f:id:teematsu:20190105134104p:plain:w600

親子関係をクラス継承ではなくオブジェクトの参照で表しているのは、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についてはまた後日...。