2009/11/23

admin generatorでrelationしているtableもfilterする

はじめに

filterって名前が紛らわしいですね。Form Filterなのか、Filter Chainなのかで全然違いますし。今回は、Form Filterに関してです。

Form Filter使っていますか?私はまったく使っていませんでしたw 使うのは、admin generatorで使うときのみでした。generator.ymlを編集して、filterさせたいフィールドだけ選択させたりくらいだけで、なかなか内部まで見れていませんでした。今でも、内部まで見ていないです。。。

しかし、ちょっと拡張してみると便利だということに気づきました。というわけで、これでもっとfilterを使おうかな、という気分になりました。今回は、propelベースで話をします。遊びプロジェクトは、doctrineで書いていたりするのですが、まだ、本番用でdoctrineを使えていないです。なんというか、doctrineで書くと、メソッドチェーンで書くことができるので、行数とか少なくて済むのですね。調子に乗ってactionに全部書いてしまって、modelにどうやって書こうか、悩んでしまうのですね。

問題

さて、デフォルトのadmin generatorの一覧画面には、右端に検索ボックスが出てきますよね。admin generatorの際に指定したmodelに関する検索です。なかなか便利だなー、とは思っていました。しかし、modelと一対なので、JOINしたりさせようとするとちょっと面倒なんですよね。一つのテーブルだけを検索できてもあまり使えないと思うので、JOINさせてfilterさせたいじゃないですか。

方法

いろんな方法が考えられるのですが、一番簡単な方法だと私が思っているのは、FormfFilterを拡張して、リレーションしたいテーブルのmodelに対応するFilterFormをmergeFormします。マジでmergeです。そして、そのFormFilterクラスのbuildCriteriaメソッドも拡張して、mergeしてあげたFilterFormでfilterしたいフィールドを追加してあげます。

準備1

つーか、簡単すぎてソースコードいらないね。。。それ用にアプリ書くの面倒ですし、プロダクトのコードそのまま持ってきたくないですし。。。と言いつつ、ここでテストアプリを作りながら、書いていくことにします。今回のは、symfony.1.2系で、propelです。すいません。
まず、schema.ymlを追加してみましょう。

propel:
  item:
    id:
    category_id:
    name:
      type: varchar(32)
      required: true
    description:
      type: longvarchar
      required: true
    created_at:
    updated_at:

  category:
    id:
    name:
      type: varchar(32)
      required: true
  is_special:
      type: boolean

data/fixtures/fixture.yml

Category:
<?php for ($i = 0; $i < 5; $i++): ?>
  category_<?php echo $i ?>:
    name: category_name_<?php echo $i ."\n" ?>
    is_special: <?php echo rand(0, 1) . "\n" ?>
<?php endfor; ?>

Item:
<?php for ($i = 0; $i < 30; $i++): ?>
  item_<?php echo $i ?>:
    name: item_name_<?php echo $i ."\n" ?>
    description: "test description"
    category_id: category_<?php echo rand(0, 2) ."\n" ?>
<?php endfor; ?>

そして、build-all-loadして、model, form, filterも作って、データベースにも突っ込んでおきましょう。ついでに、テストデータも入れておきましょう。なお、最初にdatabases.ymlなどを編集して、データベースは接続できるようにしておいてください。

$ ./symforny propel:build-all

そして、applicationを作ります。

$ ./symfony generate:app backend

で、moduleは、admin generatorで作ってしまいます。

$ ./symfony propel:generate-admin backend Item

生成されたモジュールを見ようとすると、「toStringを実装しろ」と怒られるので、実装してnameでも返してあげます。

class Category extends BaseCategory
{
  public function __toString()
  {
    return $this->name;
  }
}

before
さて、これで準備ができました。上の画像のようになりますね。fixtureでテストデータの作り方とかが、もしかしたら参考になる人がいるかもしれないですね。つまり、forしたり、randしている内容はチェックしてみてください。改行コードを入れないといけないのが、ちょっと嫌ですが。

準備2

と言いつつ、まだ準備です。次に、一覧として表示したいものを選びます。created_atとか表示しなくてもいいじゃないですか。それに、descriptionが長いときもあるので、表示させないように削ってしまいましょう。代わりにcategoryの情報が欲しいですね。is_specialというフィールドがcategoryにありますが、これを表示したいと思いますので、listに追加してあげましょう。

まず、そのためには、モデルのItemクラスに、getHogehogeを追加してあげましょう。今回は、getCategoryNameとgetCategoryIsSpecialメソッドを追加して、admin moduleからそのまま持ってくるようにしました。

class Item extends BaseItem
{
  public function getCategoryName()
  {
    if (!is_null($this->getCategory())) {
      return $this->getCategory()->getName();
    }
    return null;
  }
  public function getCategoryIsSpecial()
  {
    if (!is_null($this->getCategory())) {
      return $this->getCategory()->getIsSpecial();
    }
    return null;
  }
}

あとは、itemモジュールのgenerator.ymlを修正しましょう。

generator:
  class: sfPropelGenerator
  param:
    model_class:           Item
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              ~
    plural:                ~
    route_prefix:          item
    with_propel_route:     1

    config:
      actions: ~
      fields:  ~
      list:
        peer_method: doSelectJoinCategory
        display: [ name, category_name, category_is_special, updated_at ]
      filter:  ~
      form:    ~
      edit:    ~
      new:     ~

これでとりあえず表示したいフィールドはできました。さらに、peer_methodを変更しないとクエリーを実行しすぎなので、joinするものに変えてあげました。
ようやく準備ができたかな。labelも変えてあげたいところですが、面倒なのでry

ソースコード

さて、ようやく今回のネタです。
lib/filter/ItemFormFilter.class.php

class ItemFormFilter extends BaseItemFormFilter
{
  public function configure()
  {
    $this->mergeForm(new CategoryFormFilter());
  }

  public function buildCriteria(array $values)
  {
    $criteria = parent::buildCriteria($values);
    $criteria->addJoin(ItemPeer::CATEGORY_ID, CategoryPeer::ID);
    if (isset($values['is_special'])) {
      $criteria->add(CategoryPeer::IS_SPECIAL, $values['is_special']);
    }
    return $criteria;
  }
}

そして、apps/backend/modules/item/config/generator.yml

generator:
  class: sfPropelGenerator
  param:
    model_class:           Item
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              ~
    plural:                ~
    route_prefix:          item
    with_propel_route:     1

    config:
      actions: ~
      fields:  ~
      list:
        peer_method: doSelectJoinCategory
        display: [ name, category_name, category_is_special, updated_at ]
      filter:
        display: [ name, category_id, is_special ]
      form:    ~
      edit:    ~
      new:     ~

なんてこったい。今回の本ネタが一番短いな。。。まぁ、いいや。
after

結果上のような見た目になりましたね。実際、SQL文を見ると以下のようになっていると思います。

SELECT item.ID, item.CATEGORY_ID, item.NAME, item.DESCRIPTION, item.CREATED_AT, item.UPDATED_AT, category.ID, category.NAME, category.IS_SPECIAL FROM `item` LEFT JOIN category ON (item.CATEGORY_ID=category.ID) WHERE category.IS_SPECIAL=1 AND item.CATEGORY_ID=category.ID LIMIT 20

これで、完成です。

解説

FormFilterを拡張して、追加したいフィールドを持っているFormFilterをmergeしてあげます。これで、admin generatorを指定してあげると、filterの場所には、全フィールドが出てきますね?

そこで、filterを拡張したいモジュールのgenerator.ymlのfiltersを修正します。ここで、relationしているtableでfilterしたフィールドも追加してしまいます。もちろん対応するフィールドがFormFilterにないとエラーが出ます。前にmergeをしていましたので、実際はフィールドがFormFIlterにあることになります。

これでできたらうれしいのですが、実際は、もう少しだけ修正する必要があります。つまり、検索条件を加えるといったことです。そこで、またFormFilterに戻って、buildCriteriaをオーバーライドして、Criteriaに指定したいテーブルをJOINして、ついでに、条件を加えてあげましょう。

まとめ

admin generatorでmodelをまたいでfilterを追加する方法を説明しました。

方法としては、admin generatorで使用するFilterFormを拡張して、relationしているtableもfilterの項目に追加するようにしました。実際は、FilterFormを拡張して、追加したいFilterFormをmergeし、Criteriaを作成する際に、mergeしたFilterFormの値も追加して作成するために、buildCriteriaメソッドをオーバーライドしてみました。admin generatorが自動生成するactionなどははいじっていません。

さて、今回の実装は、具体的な実装の話でした。つまり、アプリレベルの開発の話です。どこまで抽象化するというのは、いつも悩みの種で、抽象化しすぎると複雑になるし、実際いらないということもよくあります。YAGNIです。YAGNI。抽象的にやろうと思えば、もっと複雑になるし、その辺は、DbFilterプラグインとかがよろしくやってくれそうなことを前に読んだ気もするけど、どうなんでしょうね。時間があれば使ってみます。

まぁ、今回のような話は、それぞれのアプリの仕様によることが多いので、具体的に実装してしまいましょう。

今後のちょっとした発展系としては、ヘッダの行をクリックするとソートできるようにしたらいいですね。同じような要領なので、がんばればできるでしょう。こちらは、actionを上書きしないといけないのかもしれません。

うーん。一番時間がかかってしまうのが、準備段階というのも考えものですね。。。

Bloglines feedburner