DoctrineTweetこんにちは。今日はsymfonyのフォームフィルターをご紹介します。本題に入る前に、symfonyに関連するお知らせが何点かあります。1.4は1.3から古い機能を削除したバージョンになります。大きな変更点として、以下のような内容があげられます。◆Doctrineが1.0から1.2、Propelが1.3から1.4へバージョンアップ詳しいことについては、以下のリンクをご参照ください。symfony」(日本名:もっと知りたいsymfony)です。昨年のJobeetのように12月1日から24日まで1日ずつ公開していく形式ですが、今回は前回と大きな違いがあります。なんと今回のアドベントカレンダーは日本語版も同時公開となっています。テーブル継承の活用」の翻訳をさせていただいてます。symfonyはレシピブックのような形で、symfonyをすでにお使いになっている方々の様々なニーズにも応えられる内容となっています。symfonyを、といったように、symfonyの学習の流れが出来上がってきたのかなあという感じです。現時点ですべての章が公開されているわけではありませんが、symfonyをもっと知りたいすべての方々にお勧めできる内容ですので、ぜひぜひ見てみてください!また、おかしな部分がありましたらこのブログのコメントでも結構ですのでご連絡いただけましたら幸いです。フォームフィルターとはsymfonyのフォームオブジェクトの検索用の拡張をしたクラスです。今回はこのフォームフィルターの仕組みや活用方法などをご紹介していきます。前述の通りフォームフィルターはフォームの拡張です。入力フォームを制御するためのウィジェットやバリデータを管理する機構はフォームと同様に持っており、入力フォームを作成し、入力内容のバリデーションをおこなう流れはフォームオブジェクトとほぼ同じです。フォームと同様に各ORMごとの拡張も用意されています。実際には入力された値をもとにDBから取得するオブジェクトのフィルタリングを行うのが主な使い方になると思いますので、今回はsfFormFilterDoctrineに絞ってみていきます。◆フォームフィルターを使って検索フォームを実装する何はともあれ、フォームフィルターオブジェクトを実際に作成してみましょう。1.4で動かしていきます。以下のスキーマとフィクスチャーを用意しましょう。event."今回はサンプルとして、symfonyのメソッドを検索するフォームを作ります。sfClassはクラス、sfMethodはメソッドです。1.2まではこのスキーマとフィクスチャーを読み込んでDBへ反映と各クラスの生成をするタスクはdoctrine:build-all-reloadでした。1.3/1.4からはdoctrine:buildというタスク1つにまとめられ、--allなどのオプションを付けてビルドしたものを色々と指定できるようになりました。いうわけで、コマンドを実行してスキーマとフィクスチャーを読み込ませましょう。databases.ymlは各環境に合わせて設定してください。今回はちょっとしたサンプルなのでSQLiteを使います。cc細かいオプションなどが知りたい場合は、以下のコマンドを実行してヘルプを参照してください。doctrine:buildこれで、データベースとテーブルの作成、モデル・フォーム・フォームフィルタークラスの作成、フィクスチャーの読み込みが行われました。毎度のことながら楽にできて助かります。フォームフィルタークラスはlib/filter/doctrineディレクトリ内にあります。BaseFormFilterDoctrine、sfClassFormFilter、sfMethodFormFilterの3つのクラスファイルとbaseディレクトリが含まれており、BaseFormFilterDoctrineは共通の親クラス、sfClassFormFilterとsfMethodFormFilterはそれぞれのモデルに対応したフォームフィルタークラスになります。クラスファイルを見てもいいのですが、動かすほうが楽しいと思うので、一気にモジュールとアクションを作成して動かしましょう。methodgenerate:appタスクですが、1.3/1.4からはデフォルトでエスケーピングの有効化とCSRFトークンの指定を行ってくれるようになりました。この状態でXSSとCSRF対策はされています。これでmethodモジュールができたのでindexアクション内で検索ロジックを実装しましょう。}}フォームオブジェクトをご存知の方ならば見慣れたコードでしょう。フォームオブジェクトのインスタンスを作成し、リクエストからパラメータのバインドを行う流れはフォームオブジェクトとほぼ同じです。検索なのでPOSTではなくGETで行うため、値が渡ってきているかどうかで判定しています。フォームと違うのはフォームフィルターオブジェクトに対してgetQuery()というメソッドを発行しているところでしょう。フォームでは通常、バリデーションをパスした後にsave()メソッドを実行して、入力内容をデータベースに保存します。フォームフィルターの目的は検索をすることなので、getQuery()メソッドを通じて検索条件が指定された状態のDoctrine_Queryオブジェクトを取得します。今回は触れませんが、Propelの場合はQueryがCriteriaになると思ってください。残るはテンプレートです。早く動かしたいのでできる限り簡潔にします。?>フォームフィルターはフォームを継承しているため、フォームのようにオブジェクト自身をechoすることでウィジェットのレンダリングをまとめて行えます。後はアクションですでに取得しているメソッドオブジェクトのコレクションを表示するためのものです。クラス名の表示をしている個所がありますが、今回は名前空間に対応したテーブル定義をおこなっているので、修飾されたクラス名を出すようにしましょう。getQualifiedName()メソッドがそれです。sfClassクラスに実装しましょう。}}__toString()マジックメソッドも、修飾名を返すようにオーバーライドしておきます。これでひとまず実装完了です。ブラウザからこのアクションを確認してみると、検索フォームが出来上がっていると思います。◆フォームフィルターの仕組みと拡張方法フォームフィルターの内部を探るため、フォームとフォームフィルターを見比べてみましょう。lib/filter/doctrine/base/BasesfMethodFormFilter.class.phpとlib/form/doctrine/base/BasesfMethodForm.class.phpを開いてみてください。ウィジェットとバリデータの定義が違います。入力された値の使い方が違うのでこれは当然ですね。フォームフィルター側のnameとdescriptionのウィジェットがsfWidgetFormFilterInputになっていますが、これはフォームフィルター用のウィジェットです。with_emptyオプションをtrueにすると、値が空であるかどうかを判定するためのチェックボックスが付くようになります。試しにdescriptionに対してwith_emptyをtrueに設定してみると、ブラウザでは以下のようになります。(今気づいたのですが、sfMethodFormをそのままレンダリングすると、各入力フォームのname属性がsf_method[name]のようになりますね。これではルーティングのsf_methodとかぶるので、nameFormatを変更するなりしないといけないですね。classが予約語なのでsfを付けてみたのですが、アダになっちゃいましたね。今回はsfMethodFormは使わないのでこのまま進めます。)この2つのクラスファイルの中でもっとも異なる部分と言えば、フォームフィルター側にあるgetFields()メソッドの存在でしょう。);}これはそれぞれのフィールドが、どのようにして入力された値を検索条件としてクエリ-に追加するかを指定しています。ここで使われているForeignKey、Textなどは汎用的に使われるもので、sfFormFilterDoctrineクラスにadd{$type}Queryというメソッド名で実装されています。1.4.0では以下のようなものが実装されています。◆addDateQueryfromとtoに対してどちらもis_emptyにチェックが入っている場合はNULLかどうか、それ以外の場合はfromに入力があればそれ以降、toに入力があればそれ以前の日付を指定なお、TextとNumberの場合は必ずsfWidgetFormFilterInputウィジェットを、Dateの場合はsfWidgetFormFilterDateを使うようにしてください。それ以外ではこの2つのウィジェットは使用してはいけません。is_emptyが入る関係で、method[name][text]に値が、method[name][is_empty]に空かどうかのフラグが入って渡されるので、うまく条件指定ができなくなってしまいます。値が空の場合はそもそもメソッドが呼ばれずに処理が飛ばされます。空であることに対して処理を追加することはできません。'')となり空とは判定されないので、拡張をする場合には注意が必要です。ちなみにこれらのデフォルトのタイプがどのように判定されるかは、sfDoctrineFormFilterGenerator::getType($column)に記述されています。以下のような条件でカラムの型を判定して設定しています。}}ちなみにこれらの汎用的な条件指定メソッドはもちろん自分で追加することができます。自動生成したDoctrineのフォームフィルター全てで使いたい場合はBaseFormFilterDoctrineクラスに、それぞれのクラス内でいくつも使いまわす場合はそのクラス内で、add{$type}Queryメソッドを実装すれば、それらが使えるようになります。以下はTextの前方一致版であるTextPrefixタイプをBaseFormFilterDoctrineに実装する例です。}}といってもaddTextQuery()をパクってきただけですが。引数として、ベースとなるクエリ-オブジェクト、フィールド名、値が渡されます。これらをもとにしてクエリ-を変更していきます。ここで注意したいのは、クエリ-のルートとなるモデルのみにしか指定ができないことです。sfMethodFormFilterの内部でこれらのメソッドが呼び出された場合は、sfMethodモデルのフィールドのみにしか設定ができないということです。sfMethodFormFilterでsfClassのnamespaceを検索対象にするにはどうすればいいのでしょうか。この場合の問題は、(1)どうやってクエリ-オブジェクトにsfClassをJOINするか、(2)どうやってJOINしたsfClassオブジェクトのnamespaceを指定するかの2点になります。(1)のクエリ-オブジェクトにJOINする方法です。そもそもデフォルトではどのようなクエリ-オブジェクトが作られているのでしょうか?これを作成しているのは、実際にクエリーの構築を行っている、sfFormFilterDoctrine::doBuildQuery()メソッドになります。クエリーを作成している部分をみてみましょう。...}オプションにqueryがセットされていればそのクエリーが使われ、デフォルトでは対応するモデルのテーブルからcreateQuery()メソッドでクエリーを作成しています。デフォルトではrというエイリアスが割り当てられています。$this->getTableMethod()からメソッド名を取得して、クエリーに処理をフックさせることも可能なようです。getTableMethod()メソッドはオプションにtable_methodがセットされている場合はそれを取得するようです。オプションの指定はコンストラクタの第2引数になります。...))のように指定可能です。オプションはsetOption()メソッドでもセット可能ですが、オブジェクト作成後にセットする場合は、setTableMethod()やsetQuery()メソッドを使用したほうがよいでしょう。今回は外部からではなく、sfMethodFormFilter::configure()メソッド内でクエリーをセットするようにします。}}dev環境でアプリケーションを開き、検索を行った後にWebデバッグツールバーでクエリーを確認すると、ちゃんとsfClassがJOINされていると思います。名前空間を指定するためのウィジェットとバリデータ、そして検索用のメソッドを追加していきましょう。先ほどの汎用的なタイプを指定する形式では、JOINしたモデルのフィールドを指定することはできません。そこで、namespaceに対する専用のメソッドを用意します。特定のフィールドに対して独自のメソッドを適応させる場合は、add{$column}ColumnQuery()というメソッドを実装します。ちなみにフィールドとカラムという2つの表現を使っていますが、Doctrineではフィールドはデータベース上の名前(アンダースコア)、カラムはプログラム上で扱う際の名前(キャメルケース)といったような使い分けをしています。Columnといった場合はキャメルケースになる、と覚えておいてください。is_emptyなどは使わないので、sfWidgetFormInputTextウィジェットを使用します。}}まずはnamespace用のウィジェットとバリデーターの定義を追加しました。descriptionは検索項目として必要ないので検索対象から除外しました。...)とやっていたところを、使いたいものだけ指定することができるメソッドです。このメソッドの第2引数にtrueを渡すと、ウィジェットの順番を指定した通りに並び変えてくれます。addNamespaceColumnQuery()メソッドで検索条件の指定を行っています。したギミックを加えています。名前空間を絶対指定された場合でも検索可能にし、絶対指定の場合は前方一致になるようにしてみました。getFields()メソッドは特に変更はしていませんが、これはあくまでもタイプを設定するためのもので、getFields()にないフィールドでもadd{$column}ColumnQuery()が実装されていれば受け付けてくれます。以上まででフォームフィルターの説明は終わりです。非常に便利な機能なのでぜひぜひ使っていただければと思います。symfonyもぜひみてください!※名前空間で思い出したのですが、12月15日にモダンPHP勉強会が開催され、そこで名前空間についての発表を行います。当日はUstreamでも配信予定ですので、よろしければ見てください。byスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。