読者です 読者をやめる 読者になる 読者になる

モノノフ日記

普通の日記です

Jobeet - 12日目: アドミンジェネレータ

昨日Jobeetに追加した機能は求職者や求人側のフロントエンドアプリケーションがより便利になるものでした。バックエンドアプリケーションについて少しお話しします。
今日はsymfonyの機能的なアドミンジェネレータを使うことでちょうど1時間くらいでJobeetに完全なバックエンドのインタフェースを開発するつもりです。

Backendの作成

一番最初のステップはバックエンドアプリケーションを作成することです。もし記憶力が良いなら、generate:appタスクの使い方を覚えるべきです。

$ php symfony generate:app --escaping-strategy=on --csrf-secret=UniqueSecret1 backend

たとえバックエンドアプリケーションがJobeetの管理者だけが使う場合でも、symfonyに組み込まれているセキュリティ要素は有効にしておきます。

もしパスワードに$のような特殊文字を使いたいなら、CLI上で適切なエスケープが必要になります。

$ php symfony generate:app --csrf-secret=Unique\$ecret backend

バックエンドアプリケーションはこれで利用できるようになります、本番環境は http://jobeet.localhost/backend.php/ で、開発環境は http://jobeet.localhost/backend_dev.php/ で動作します。

フロントエンドアプリケーションを生成するとき、生成物の中にindex.phpがあります。1ディレクトリに1つのindex.phpしか持てないので、symfonyは一番最初のフロントコントローラ用にindex.phpを生成し、その後はアプリケーション名を利用します。

もしpropel:data-loadタスクを使ってデータをリロードしようと試しても動作しないでしょう。その理由はJobeetJob::save()メソッドがfrontendアプリケーションのapp.yml設定ファイルを利用する必要があるからです。現在2つのアプリケーションが存在し、symfonyは最初に見つけた方を使います。それはbackend側のファイルです。
しかし8日目で見たように、設定は異なったレベルで構成できます。 apps/frontend/config/app.ymlファイルを config/app.ymlへ移動させることによって、設定は全てのアプリケーション間で共有され、問題は解決します。この変更で、アドミンジェネレータ内で広範囲のモデルクラスを使うことになります、そのためバックエンドアプリケーション内のapp.ymlファイルで変数を定義することが必要となります。

propel:data-loadタスクは--applicationオプションを引数にとれます。もしアプリケーションにおいて特定の設定が必要なら、下記コマンドが使えます。

$ php symfony propel:data-load --application=frontend

Backendモジュール

フロントエンドアプリケーション用に、モデルクラスをベースとしたCRUDモジュールを生成するのにpropel:generate-moduleタスクを利用しました。バックエンド用には、propel:generate-adminタスクを使うことでモデルクラス用の完全に動作するバックエンドのインターフェースが生成されます。

$ php symfony propel:generate-admin backend JobeetJob --module=job
$ php symfony propel:generate-admin backend JobeetCategory --module=category

これら2つのコマンドはJobeetJob、JobeetCategoryモデルそれぞれに対応するjob、categoryモジュールを生成します。
任意の --module オプションでタスクがデフォルトで付ける名前を上書きできます。(指定していないとJobeetJobクラスの場合jobeet_jobという名前になります)
ひそかに、タスクは各モジュール間のカスタムルートも生成します。

# apps/backend/config/routing.yml
jobeet_job:
  class: sfPropelRouteCollection
  options:
    model:                JobeetJob
    module:               job
    prefix_path:          job
    column:               id
    with_wildcard_routes: true

アドミンインターフェースの主な目標はモデルオブジェクトのライフサイクルの管理であるため、アドミンジェネレータによって使われるルートクラスがsfPropelRouteCollectionであることは何ら驚くべきことではありません
ルートの定義は上記のサンプルで出てこないようなオプションを定義します。

  • prefix_path
    • 生成されたルートのためのprefixパスを定義します(例えば、編集ページでは /job/1/edit のようになります)
  • column
    • オブジェクトを参照するため、URLで使っているカラムを定義します
  • with_wildcard_routes
    • アドミンインターフェースがより多くの回数のクラシックなCRUD操作をするのであれば、このオプションを有効にすることでルートを編集することなしでより多くのオブジェクトやコレクションを定義できます

いつものことですが、新しいタスクを使う前にヘルプを読むのは良い傾向です。

$ php symfony help propel:generate-admin

そのタスクの典型的な使い方だけでなく、全ての引数、オプションの使い方を見れます。

Backendの見た目

即座に、生成したモジュールは使えるようになります。
http://jobeet.localhost/backend_dev.php/job
http://jobeet.localhost/backend_dev.php/category

アドミンモジュールは以前生成したシンプルなモジュールよりもより多くの要素を持っています。1行もPHPコードを書くことなしに各モジュールは下記の素晴らしい要素を提供します。

  • オブジェクトのリストのペジネート
  • ソーティング処理
  • フィルタ処理
  • オブジェクトの生成、編集、削除
  • 選択したオブジェクトに対するバッチ処理
  • フォームバリデーション
  • ユーザへ直接フィードバックするFlashメッセージ
  • などなど...

アドミンジェネレータはパッケージを構成するためのシンプルなバックエンドインターフェースを生成するのに必要な全ての要素を提供します。
より良いユーザ体験にさせるため、デフォルトのバックエンドをカスタマイズする必要があります。シンプルなメニューを追加して、異なったモジュール間でのナビゲーションを簡単にさせます。
デフォルトのlayout.phpを下記コードのように置き換えます。

// apps/backend/templates/layout.php
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <title>Jobeet Admin Interface</title>
    <link rel="shortcut icon" href="/favicon.ico" />
    <?php use_stylesheet('admin.css') ?>
    <?php include_javascripts() ?>
    <?php include_stylesheets() ?>
  </head>
  <body>
    <div id="container">
      <div id="header">
        <h1>
          <a href="<?php echo url_for('@homepage') ?>">
            <img src="/images/logo.jpg" alt="Jobeet Job Board" />
          </a>
        </h1>
      </div>
 
      <div id="menu">
        <ul>
          <li>
            <?php echo link_to('Jobs', '@jobeet_job') ?>
          </li>
          <li>
            <?php echo link_to('Categories', '@jobeet_category') ?>
          </li>
        </ul>
      </div>
 
      <div id="content">
        <?php echo $sf_content ?>
      </div>
 
      <div id="footer">
        <img src="/images/jobeet-mini.png" />
        powered by <a href="http://www.symfony-project.org/">
        <img src="/images/symfony.gif" alt="symfony framework" /></a>
      </div>
    </div>
  </body>
</html>

このレイアウトはadmin.cssを使います。このファイルは4日目で他のスタイルシートと一緒にインストールしたので、web/css/ディレクトリにすでに存在しているはずです。
f:id:Kiske:20090309153711p:image

最後に、routing.yml内でsymfonyのデフォルトホームページを変更します。

# apps/backend/config/routing.yml
homepage:
  url:   /
  param: { module: job, action: index }

symfonyキャッシュ

好奇心が旺盛な人ならば、おそらくすでにタスクが生成したapps/backend/modulesディレクトリ下のファイルを開いているでしょう。もしそうでないなら、今開いてみましょう。ビックリしましたか?テンプレートディレクトリは空で、actions.class.phpファイルも同じようにもぬけの殻です。

<?php
// apps/backend/modules/job/actions/actions.class.php
require_once dirname(__FILE__).'/../lib/jobGeneratorConfiguration.class.php';
require_once dirname(__FILE__).'/../lib/jobGeneratorHelper.class.php';
 
class jobActions extends autoJobActions
{
}

どうやって動いているのでしょうか?よく見てみると、jobActionsクラスはautoJobActionsクラスを拡張していることに気づくでしょう。autoJobActionsクラスは存在しなければsymfonyが自動で生成するクラスです。そのクラスは cache/backend/dev/modules/autoJob/ ディレクトリで見ることができ、本当のモジュールを含んでいます。

<?php
// cache/backend/dev/modules/autoJob/actions/actions.class.php
class autoJobActions extends sfActions
{
  public function preExecute()
  {
    $this->configuration = new jobGeneratorConfiguration();
 
    if (!$this->getUser()->hasCredential(
      $this->configuration->getCredentials($this->getActionName())
    ))
    {
 
// ...

アドミンジェネレータを動作させる方法は既知のパターンであるということに気づくべきです。実際には、既に学習したモデルとフォームクラスとほとんど同じです。モデルのスキーマ定義に基づいて、symfonyはモデルとフォームクラスを生成します。アドミンジェネレータでは、生成されたモジュールはモジュール内にある config/generator.yml ファイルを編集することで設定できます。

# apps/backend/modules/job/config/generator.yml
generator:
  class: sfPropelGenerator
  param:
    model_class:           JobeetJob
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              ~
    plural:                ~
    route_prefix:          jobeet_job
    with_propel_route:     1
 
    config:
      actions: ~
      fields:  ~
      list:    ~
      filter:  ~
      form:    ~
      edit:    ~
      new:     ~

generator.ymlを更新するたびに、symfonyはキャッシュを再生成します。今日見たように生成されたモジュールをカスタマイズすることは簡単で速く、そして面白いです。

キャッシュの自動再生成は開発環境のみで起こります。本番環境では、cache:clearタスクを使って手動でキャッシュ削除する必要があります

Backendの設定

アドミンモジュールはgenerator.ymlファイルのconfigキーを編集することでカスタマイズできます。設定は7つのセクションで構成されます。

  • actions
    • リストとフォームで検出できるアクション用のデフォルト設定
  • fields
    • フィールド用のデフォルト設定
  • list
    • リストの設定
  • filter
    • フィルタの設定
  • form
    • 作成/編集フォームの設定
  • edit
    • 編集ページ特有の設定
  • new
    • 作成ページ特有の設定

それではカスタマイズを始めましょう。

タイトル設定

categoryモジュールのlist、edit、newセクションにtitleオプションを定義します。

# apps/backend/modules/category/config/generator.yml
config:
  actions: ~
  fields:  ~
  list:
    title: Category Management
  filter:  ~
  form:    ~
  edit:
    title: Editing Category "%%name%%"
  new:
    title: New Category

editセクションのtitleは動的な値を持ちます。%%で囲まれた全ての文字列は対応するオブジェクトのカラム値に置き換えられます。
f:id:Kiske:20090309175721p:image

jobモジュール用の設定もほとんど同じです。

# apps/backend/modules/job/config/generator.yml
config:
  actions: ~
  fields:  ~
  list:
    title: Job Management
  filter:  ~
  form:    ~
  edit:
    title: Editing Job "%%company%% is looking for a %%position%%"
  new:
    title: Job Creation

フィールド設定

異なるビュー(list, new, edit)はフィールドから成っています。1つのフィールドはモデルクラスのカラムや後で見るようなバーチャルカラムとなります。
デフォルトのフィールド設定はfieldsセクションでカスタマイズできます。

# apps/backend/modules/job/config/generator.yml
config:
  fields:
    is_activated: { label: Activated?, help: Whether the user has activated the job, or not }
    is_public:    { label: Public? }

f:id:Kiske:20090309180629p:image

fieldsセクションは全てのビューのフィールド設定を上書きします。これはis_activatedフィールドのlabelはlist,edit,newのビュー上で変更されるということを意味しています。
アドミンジェネレータの設定はカスケード方式での設定に基づいています。例えば、listビューだけのラベルを変更したいならば、listセクションのfieldsオプションに下記のように定義できます。

# apps/backend/modules/job/config/generator.yml
config:
  list:
    fields:
      is_public:    { label: "Public? (label for the list)" }

メインのfieldsセクション下のいかなる設定もビュー固有の設定によって上書きされます。上書きルールは次の通りです。

  • newとeditはformとfieldsから継承
  • listはfieldsから継承
  • filterはfieldsから継承

フォームセクション(form, edit, new)では、labelとhelpオプションはformクラス内の物を上書きします。

リストビュー設定

display

デフォルトでは、リストビューのカラムはschema.ymlファイル順にモデルの全カラムとなっています。displayオプションは表示されるカラム順を定義することでデフォルトを上書きします。

# apps/backend/modules/category/config/generator.yml
config:
  list:
    title:   Category Management
    display: [=name, slug]

nameカラムの前にある = は文字列をリンクに変換するための仕様です。
f:id:Kiske:20090309182555p:image

jobモジュールにも同じ処理を加えて、もっと読みやすくしましょう。

# apps/backend/modules/job/config/generator.yml
config:
  list:
    title:   Job Management
    display: [company, position, location, url, is_activated, email]
layout

リストは異なったレイアウトで表示できます。デフォルトではレイアウトは表形式で、各コラム値がそれぞれのテーブルカラムとなっています。しかしjobモジュールでは他の組み込みレイアウトであるstackedレイアウトを使った方がよくなります。

# apps/backend/modules/job/config/generator.yml
config:
  list:
    title:   Job Management
    layout:  stacked
    display: [company, position, location, url, is_activated, email]
    params:  |
      %%is_activated%% <small>%%category_id%%</small> - %%company%%
       (<em>%%email%%</em>) is looking for a %%=position%% (%%location%%)

stackedレイアウトでは、各オブジェクトはparamsオプションで定義した一連の文字列として表されます。

displayオプションはユーザによって分類されたカラムを定義することがまだ必要です。

バーチャルカラム

この設定で、%%category_id%%セグメントはcategoryプライマリキーで置き換えることが可能です。しかしカテゴリ名を表示することでもっと意味を持たせれます。
%%表記を使うときはいつでも、変数はデータベーススキーマ内に存在するカラムと一致する必要はありません。アドミンジェネレータはモデルクラスの関連するgetterを見つけるためだけに必要です。
カテゴリ名を表示するためにはJobeetJobモデルクラスにgetCategoryName()メソッドを定義し、%%category_id%%を%%category_name%%に置き換えれば可能となります。
しかしJobeetJobクラスはすでに関連するカテゴリーオブジェクトを返すgetJobeetCategory()メソッドを持っています。もし%%jobeet_category%%を使うならば、JobeetCategoryクラスが持っているオブジェクトを文字列に変換する__toString()マジックメソッドが動作します。

# apps/backend/modules/job/config/generator.yml
%%is_activated%% <small>%%jobeet_category%%</small> - %%company%%
 (<em>%%email%%</em>) is looking for a %%=position%% (%%location%%)

f:id:Kiske:20090309185132p:image

sort

管理者としては、おそらく最新の投稿された仕事を見ることがより興味のあることだと思います。sortオプションを追加することでデフォルトでソートするカラムを設定することができます。

# apps/backend/modules/job/config/generator.yml
config:
  list:
    sort: [expires_at, desc]
max_per_page

デフォルトでは、リストはペジネートされ、各ページに20アイテムを含みます。max_per_pageオプションでそれらの値を変更できます。

# apps/backend/modules/job/config/generator.yml
config:
  list:
    max_per_page: 10

f:id:Kiske:20090309185615p:image

batch_actions

リストでは、アクションはいくつかのオブジェクトを実行することが可能です。これらのバッチオプションはcategoryモジュールを必要としません。削除してみましょう。

# apps/backend/modules/category/config/generator.yml
config:
  list:
    batch_actions: {}

f:id:Kiske:20090309190123p:image

batch_actionsオプションはリストのバッチアクションを定義します。空の配列は要素を除去することを可能にします。
デフォルトでは各モジュールはフレームワークが定義したdeleteバッチアクションを持っています。しかしjobモジュールでは選択した仕事の有効性を30日延ばす方法が必要となるのでそれを見せかけてみましょう。

# apps/backend/modules/job/config/generator.yml
config:
  list:
    batch_actions:
      _delete:    ~
      extend:     ~

_から始まる全てアクションはフレームワークによって提供されるアクションです。もしブラウザをリフレッシュして、extendバッチアクションを選択したなら、symfonyはexecuteBatchExtend()メソッドを生成するように促すエラーとともに例外を投げるでしょう。

<?php
// apps/backend/modules/job/actions/actions.class.php
class jobActions extends autoJobActions
{
  public function executeBatchExtend(sfWebRequest $request)
  {
    $ids = $request->getParameter('ids');
 
    $jobs = JobeetJobPeer::retrieveByPks($ids);
 
    foreach ($jobs as $job)
    {
      $job->extend(true);
    }
 
    $this->getUser()->setFlash('notice', 'The selected jobs have been extended successfully.');
 
    $this->redirect('@jobeet_job');
  }
}

選択されたプライマリキーはidsというリクエストパラメータに保存されています。選択された仕事ごとにJobeetJob::extend()メソッドを期限切れチェックを回避した特別な引数とともにコールさせます。
このアカウント内の新しい引数を持つようにextend()メソッドを更新します。

<?php
// lib/model/JobeetJob.php
class JobeetJob extends BaseJobeetJob
{
  public function extend($force = false)
  {
    if (!$force && !$this->expiresSoon())
    {
      return false;
    }
 
    $this->setExpiresAt(time() + 86400 * sfConfig::get('app_active_days'));
    $this->save();
 
    return true;
  }
 
  // ...
}

全ての仕事を延長した後、ユーザはjobモジュールのホームページにリダイレクトされます。
f:id:Kiske:20090309191713p:image

object_actions

リスト内では、シングルオブジェクトとして実行できるアクション用の追加のカラムがあります。categoryモジュールでは編集可能なカテゴリー名がリンクを持つようになっているのを削除してみましょう。また、リストから1つのディレクトリを削除するのは実際には必要ありません。

# apps/backend/modules/category/config/generator.yml
config:
  list:
    object_actions: {}

jobモジュールでは存在しているアクションを維持し、そしてバッチアクションに追加したように新しくextendアクションを追加してみましょう。

# apps/backend/modules/job/config/generator.yml
config:
  list:
    object_actions:
      extend:     ~
      _edit:      ~
      _delete:    ~

バッチアクションのように、_deleteと_editアクションはフレームワークによって定義されます。extendリンクを動作させるためにlistExtend()アクションを定義する必要があります。

<?php
// apps/backend/modules/job/actions/actions.class.php
class jobActions extends autoJobActions
{
  public function executeListExtend(sfWebRequest $request)
  {
    $job = $this->getRoute()->getObject();
    $job->extend(true);
 
    $this->getUser()->setFlash('notice', 'The selected jobs have been extended successfully.');
 
    $this->redirect('@jobeet_job');
  }
 
  // ...
}

f:id:Kiske:20090309194040p:image

actions

オブジェクトのリストやシングルオブジェクトへのアクションのリンクのやり方はもう見てきました。actionsオプションは新しいオブジェクトを生成するようなオブジェクトが全く無いアクションを定義します。デフォルトのnewアクションを削除して、投稿されてから60日以上経った全ての仕事を削除する新しいアクションを追加してみましょう。

# apps/backend/modules/job/config/generator.yml
config:
  list:
    actions:
      deleteNeverActivated: { label: Delete never activated jobs }

これまでのところ、全てのアクションは~で定義されています、これらはsymfonyが自動的にアクションを構築することを意味します。各アクションは配列のパラメータで定義することでカスタマイズできます。labelオプションはsymfonyによって生成されたデフォルトラベルを上書きします。
デフォルトでは、リンクをクリックしたときに実行されたアクションはlistを接頭字としたアクション名となります。
jobモジュールのアクションにlistDeleteNeverActivatedアクションを生成します。

<?php
// apps/backend/modules/job/actions/actions.class.php
class jobActions extends autoJobActions
{
  public function executeListDeleteNeverActivated(sfWebRequest $request)
  {
    $nb = JobeetJobPeer::cleanup(60);
 
    if ($nb)
    {
      $this->getUser()->setFlash('notice', sprintf('%d never activated jobs have been deleted successfully.', $nb));
    }
    else
    {
      $this->getUser()->setFlash('notice', 'No job to delete.');
    }
 
    $this->redirect('@jobeet_job');
  }
 
  // ...
}

昨日定義したJobeetJobPeer::cleanup()メソッドを再利用します。これはMVCパターンが提供する再利用性の良いサンプルです。

actionパラメータへ渡す実行するアクションを変更できます。

deleteNeverActivated: { label: Delete never activated jobs, action: foo }

f:id:Kiske:20090309195809p:image

peer_method

Webデバッグツールバーで確認すると、仕事のページを表示するために必要なデータベースへのリクエスト数は14です。
そのリクエスト数をクリックすれば、ほとんどのリクエストが各仕事ごとにカテゴリー名で取得しているのが見れます。
f:id:Kiske:20090310105446p:image
データベースへのリクエスト数を減らすために、peer_methodオプションを使って仕事を取得するのに利用するデフォルトメソッドを変更できます。

# apps/backend/modules/job/config/generator.yml
config:
  list:
    peer_method: doSelectJoinJobeetCategory

doSelectJoinJobeetCategory()メソッドはjobテーブルとcategoryテーブル間にJOINを追加し、各仕事に関連するカテゴリーオブジェクトを自動で生成します。
リクエスト数は4回に減っています。
f:id:Kiske:20090310105445p:image

フォームビューの設定

フォームビューの設定は3つのセクションから構成されます。form、edit、newセクションです。それらは全て同じ設定の機能であるので、editやnewセクションの替わりにformセクションだけを使います。

display

listで行ったように、displayオプションを使って表示されるフィールドの順番を変更することができます。しかし、表示されたフォームがクラスで定義されている場合、予期しないバリデーションエラーを導いてしまうのでフィールドを削除することは試せません。
フォームビュー用のdisplayオプションはグループ内でのフィールドの並び替えに使うことができます。

# apps/backend/modules/job/config/generator.yml
config:
  form:
    display:
      Content: [category_id, type, company, logo, url, position, location, description, how_to_apply, is_public, email]
      Admin:   [_generated_token, is_activated, expires_at]

上記の設定は2つのグループ(ContentとAdmin)を定義しており、各フォームフィールドの一部を含んでいます。
f:id:Kiske:20090310133532p:image

ジョブフォームの定義内でセットされていないため、Adminグループのカラムはブラウザからまだ見えません。アドミンアプリケーションでカスタムジョブフォームクラスを定義すれば、それらは数セクション内に現れるでしょう。

アドミンジェネレータは多対多のリレーションシップのサポートは組み込まれています。カテゴリーフォームでは、名前やスラッグの入力、ドロップダウンボックスから関連する会社を選択します。ページ上で関連を編集することが意味がないのであれば削除してしまいましょう。

<?php
// lib/form/JobeetCategoryForm.class.php
class JobeetCategoryForm extends BaseJobeetCategoryForm
{
  public function configure()
  {
    unset($this['jobeet_category_affiliate_list']);
  }
}

「Virtual」クラス

ジョブフォーム用のdisplayオプション内で、アンダースコアで始まる_generated_tokenフィールドを定義しました。これはこのフィールドのレンダリングをカスタムパーシャルである _generated_token.php で処理させるということを意味します。
下記コードを使ってパーシャルを作成します。

<?php
// apps/backend/modules/job/templates/_generated_token.php
<div class="sf_admin_form_row">
  <label>Token</label>
  <?php echo $form->getObject()->getToken() ?>
</div>

パーシャル内では参照しているフォームには$formでアクセスでき、関連するオブジェクトにはgetObject()メソッドを経由してアクセスします。

チルダが接頭辞に使われているコンポーネントレンダリングをデリゲートできます。

class

管理者が使うフォームであるため、ユーザが利用するジョブフォームより多くの情報を表示させます。しかし今のところは、JobeetJobFormクラス内で削除されているのでいくつかの要素はフォーム上で表示していません。
フロントエンドとバックエンド間で異なったフォームを持つためには、2つのフォームクラスを作成することが必要となります。JobeetJobFormクラスを拡張したBackendJobeetJobFormクラスを作成しましょう。同じhiddenフィールドを持たないようにするため、JobeetJobFormクラスを少しリファクタリングして、BackendJobeetJobFormクラスにunset()ステートメントのメソッドをオーバーライドする必要があります。

<?php
// lib/form/JobeetJobForm.class.php
class JobeetJobForm extends BaseJobeetJobForm
{
  public function configure()
  {
    $this->removeFields();
 
    $this->validatorSchema['email'] = new sfValidatorEmail();
 
    // ...
  }
 
  protected function removeFields()
  {
    unset(
      $this['created_at'], $this['updated_at'],
      $this['expires_at'], $this['is_activated'],
      $this['token']
    );
  }
}
 
// lib/form/BackendJobeetJobForm.class.php
class BackendJobeetJobForm extends JobeetJobForm
{
  public function configure()
  {
    parent::configure();
  }
 
  protected function removeFields()
  {
    unset(
      $this['created_at'], $this['updated_at'],
      $this['token']
    );
  }
}

アドミンジェネレータが使うデフォルトフォームはclassオプションで設定をオーバーライドできます。

# apps/backend/modules/job/config/generator.yml
config:
  form:
    class: BackendJobeetJobForm

新しいクラスを追加したときはキャッシュの削除を忘れずに。

editフォームはまだ少し使いづらいです。現在アップロードされたロゴはどこにも表示されず、削除することもできません。sfWidgetFormInputFileEditableウィジェットはシンプルなファイル入力の編集機能を追加します。

<?php
// lib/form/BackendJobeetJobForm.class.php
class BackendJobeetJobForm extends JobeetJobForm
{
  public function configure()
  {
    parent::configure();
 
    $this->widgetSchema['logo'] = new sfWidgetFormInputFileEditable(array(
      'label'     => 'Company logo',
      'file_src'  => '/uploads/jobs/'.$this->getObject()->getLogo(),
      'is_image'  => true,
      'edit_mode' => !$this->isNew(),
      'template'  => '<div>%file%<br />%input%<br />%delete% %delete_label%</div>',
    ));
 
    $this->validatorSchema['logo_delete'] = new sfValidatorPass();
  }
 
  // ...
}

sfWidgetFormInputFileEditableウィジェットレンダリングや要素のカスタマイズのためのオプションを指定できます。

  • file_src
    • 現在アップロードされたファイルへのWebパス
  • is_image
  • edit_mode
    • フォームが編集モードであるかどうか
  • with_delete
  • template

f:id:Kiske:20090310141725p:image

アドミンジェネレータの見ためは生成されたテンプレートでたくさんのclassやid属性を定義しているので非常に簡単にカスタマイズできます。例えば、ロゴフィールドはsf_admin_form_field_logoクラスでカスタマイズされています。各フィールドはsf_admin_textやsf_admin_booleanのようなフィールドのタイプに依存したクラス属性を持っています。

edit_modeオプションはsfPropel::isNew()メソッドを使います。
isNew()メソッドはフォームのモデルオブジェクトが新しく生成されたものであればtrueを、それ以外はfalseを返します。埋め込みオブジェクトのステータスに依存する異なったウィジェットやバリデータを持つ必要があるとき、とても役に立ちます。

フィルタの設定

フィルタの設定はフォームビューの設定とよく似ています。事実問題として、フィルタはフォームのようなものです。フォームに関して、クラスはpropel:build-allタスクによって生成されます。propel:build-filtersタスクを使うと再生成できます。
フォームフィルタクラスは lib/filter/ ディレクトリ下に位置されていて、各モデルクラスはフィルタフォームクラスと関連づけられています(JobeetJobFormFilterとJobeetJobFormのように)。
categoryモジュール用のフィルタを完全に削除してみましょう。

# apps/backend/modules/category/config/generator.yml
config:
  filter:
    class: false

jobモジュール用に、いくつかのフィルタを削除してみましょう。

# apps/backend/modules/job/config/generator.yml
filter:
  display: [category_id, company, position, description, is_activated, is_public, email, expires_at]

フィルタは常に任意設定であるため、表示されるフィールドを構成するためにフィルタフォームクラスをオーバーライドする必要はありません。
f:id:Kiske:20090310144632p:image

アクションの設定

設定が十分でないとき、拡張した要素を見るためにアクションクラスに新しいメソッドを追加できます。しかし生成されたアクションメソッドをオーバーライドすることもできます。

メソッド 説明
exexuteIndex() listビューアクション
executeFilter() フィルタの更新
executeNew() 新しいビューアクション
executeCreate() 新しいジョブの作成
executeEdit() editビューアクション
executeUpdate() ジョブの更新
executeDelete() ジョブの削除
executeBatch() バッチアクションの実行
executeBatchDelete() _deleteバッチアクションの実行
processForm() ジョブフォームの処理
getFilters() 現在のフィルタを返す
setFilters() フィルタのセット
getPager() リストページャを返す
getPage() ページャページの取得
setPage() ページャページをセット
buildCriteria() リスト用のCriteriaをビルド
addSortCriteria() リスト用のソートCriteriaを追加
getSort() ソートしているカラムを返す
setSort() ソートするカラムのセット

生成された各メソッドがたった1つであるなら、たくさんのコードをコピー&ペーストすることなしに動作を変更するのは簡単です。

テンプレートの設定

アドミンジェネレータによってHTMLコード内に追加されたclassやid属性のおかげで生成されたテンプレートをカスタマイズする方法はすでに確認しました。
classに関しては、オリジナルテンプレートで上書きもできます。テンプレートはプレーンPHPファイルでありPHPクラスファイルではないので、モジュール内で同じ名前のテンプレートを作成することで上書きされるようになります(例えば、apps/backend/modules/job/templates/ ディレクトリはjobアドミンモジュールのためのものです)

テンプレート 説明
_assets.php テンプレートで利用するCSSとJSファイルの表示
_filters.php フィルタボックスの表示
_filters_field.php シングルフィルタフィールドの表示
_flashes.php Flashメッセージの表示
_form.php フォームの表示
_form_actions.php フォームアクションの表示
_form_field.php シングルフォームフィールドの表示
_form_fieldset.php フォームフィールドセットの表示
_form_footer.php フォームフッタの表示
_form_header.php フォームヘッダの表示
_list.php リストの表示
_list_actions.php リストアクションの表示
_list_batch_actions.php リストバッチアクションの表示
_list_field_boolean.php リスト内のシングルブーリアンフィールドの表示
_list_footer.php リストフッタの表示
_list_header.php リストヘッダの表示
_list_td_actions.php テーブル行向けのオブジェクトアクションの表示
_list_td_batch_actions.php テーブル行向けのチェックボックスの表示
_list_td_stacked.php テーブル行向けのstackedレイアウトの表示
_list_td_tabular.php リスト用のシングルフィールドの表示
_list_th_stacked.php ヘッダ向けのシングルカラム名の表示
_list_th_tabular.php ヘッダ向けのシングルカラム名の表示
_pagination.php リストペジネーションの表示
editSuccess.php editビューの表示
indexSuccess.php listビューの表示
newSuccess.php newビューの表示

最終的な設定

# apps/backend/modules/job/config/generator.yml
generator:
  class: sfPropelGenerator
  param:
    model_class:           JobeetJob
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              ~
    plural:                ~
    route_prefix:          jobeet_job
    with_propel_route:     1
 
    config:
      actions: ~
      fields:
        is_activated: { label: Activated?, help: Whether the user has activated the job, or not }
        is_public:    { label: Public? }
      list:
        title:         Job Management
        layout:        stacked
        display:       [company, position, location, url, is_activated, email]
        params:  |
          %%is_activated%% <small>%%jobeet_category%%</small> - %%company%%
           (<em>%%email%%</em>) is looking for a %%=position%% (%%location%%)
        max_per_page:  10
        sort:          [expires_at, desc]
        batch_actions:
          _delete:    ~
          extend:     ~
        object_actions:
          extend:     ~
          _edit:      ~
          _delete:    ~
        actions:
          deleteNeverActivated: { label: Delete never activated jobs }
        peer_method:  doSelectJoinJobeetCategory
      filter:
        display: [category_id, company, position, description, is_activated, is_public, email, expires_at]
      form:
        class:     BackendJobeetJobForm
        display:
          Content: [category_id, type, company, logo, url, position, location, description, how_to_apply, is_public, email]
          Admin:   [_generated_token, is_activated, expires_at]
      edit:
        title: Editing Job "%%company%% is looking for a %%position%%"
      new:
        title: Job Creation
 
# apps/backend/modules/category/config/generator.yml
generator:
  class: sfPropelGenerator
  param:
    model_class:           JobeetCategory
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              ~
    plural:                ~
    route_prefix:          jobeet_category
    with_propel_route:     1
 
    config:
      actions: ~
      fields:  ~
      list:
        title:   Category Management
        display: [=name, slug]
        batch_actions: {}
        object_actions: {}
      filter:
        class: false
      form:
        actions:
          _delete: ~
          _list:   ~
          _save:   ~
      edit:
        title: Editing Category "%%name%%"
      new:
        title: New Category

これら2つの設定ファイルを使うだけで、ものの数分でJobeet用のすばらしいバックエンドインターフェースを開発しました。

すでに知っているかもしれませんが、YAMLファイル内で何か設定するときプレーンPHPコード使うことも可能です。アドミンジェネレータ用には apps/backend/modules/job/lib/jobGeneratorConfigguration.class.php ファイルを編集することで可能です。YAMLファイルのようにいくつかのオプションが使えますがPHPインターフェースです。メソッド名を学習するには、生成されるcache/backend/dev/modules/autoJob/lib/BaseJobGeneratorConfiguration.class.php ファイルの基本クラスを見てください。