Caution: You are browsing the legacy 1.x part of this website.
This version of symfony is not maintained anymore. If some of your projects still use this version, consider upgrading.

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

Jobeet

11日目に Jobeet に追加したもので、フロントエンドアプリケーションは職を探す人にも、提供する人にもとても便利なものになりました。ここで少しバックエンドアプリケーションについて話します。今日は、symfony のアドミンジェネレータを使って、1時間で Jobeet の完全なバックエンドインターフェイスを開発しましょう。

バックエンドの作成

一番最初のステップは backend アプリケーションを作ることです。記憶力のよい方は generate:app タスクでこれを行う方法を覚えていることでしょう。

$ php symfony generate:app backend

prod 環境では http://jobeet.localhost/backend.php/dev 環境では http://jobeet.localhost/backend_dev.php/ を通してバックエンドアプリケーションを利用できます。

note

フロントエンドアプリケーションを作ったとき、運用環境のフロントコントローラの名前は index.php でした。ディレクトリごとに index.php は1つしか置けないので、symfony は一番最初のフロントコントローラに index.php を作り、その後はアプリケーションの名前を使います。

doctrine:data-load タスクでデータフィクスチャをリロードしようとしても、動かないでしょう。JobeetJob::save() メソッドは frontend アプリケーションの app.yml 設定ファイルにアクセスすることが必要とするからです。今は2つのアプリケーションがあるので、symfony は最初に見つかるアプリケーションである backend アプリケーションを使います。

しかし8日目でみたように、異なるレベルで設定を変更できます。apps/frontend/config/app.yml ファイルの内容を config/app.yml ファイルに移動させることで、すべてのアプリケーションの間で設定は共有され問題は解決されます。アドミンジェネレータでモデルクラスを広範囲で使うので、今変更しましょう。また、バックエンドアプリケーションの app.yml ファイルで定義される変数が必要になります。

tip

doctrine:data-load タスクは --application オプションもとります。ですので、あるアプリケーションから特定の設定が必要な場合、次のように実行します。

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

バックエンドモジュール

フロントエンドアプリケーションでは、モデルクラスにもとづいた基本的な CRUD モジュールをブートストラップするために、 doctrine:generate-module タスクを使いました。バックエンドでは、モデルクラスの完全なバックエンドインターフェイスを生成する doctrine:generate-admin タスクを使います。

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

これら2つのコマンドは JobeetJobJobeetCategory モデルクラスに対してそれぞれ jobcategory モジュールを生成します。

--module オプションを追加すると、タスクによってデフォルトで生成される module の名前がオーバーライドされます (そうしなければ JobeetJob クラスに対して jobeet_job になります)。

モジュール作成の裏側では、タスクはそれぞれのモジュール用のカスタムのルートも作成しました。

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

アドミンインターフェイスの主な目的ははモデルオブジェクトのライフサイクルの管理なので、アドミンジェネレータによって使われるルートクラスが sfDoctrineRouteCollection であるのは驚くことではありません。

ルートは以前には見かけなかったオプションも定義します。

  • prefix_path: 生成されるルートの前につけられるパスを定義する (たとえば、編集ページは /job/1/edit のようになる)。
  • column: オブジェクトを参照するリンク用の URL で使うテーブルのカラムを定義する
  • with_wildcard_routes: アドミンインターフェイスは典型的な CRUD オペレーション以上の機能を持つため、このオプションでルートの編集なしにさらなるオブジェクトやコレクションアクションを定義することを許可します。

tip

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

$ php symfony help doctrine:generate-admin

このコマンドによって他のソフトウェアの典型的な使い方の例と同じようにタスクのすべての引数とオプションが表示されます。

バックエンドの外観

生成されたモジュールはすぐに利用できます。

http://jobeet.localhost/backend_dev.php/job
http://jobeet.localhost/backend_dev.php/category

アドミンモジュールは以前に生成したシンプルなモジュールよりも多くの機能を持ちます。PHP を一行も書くことなく、それぞれのモジュールはこれらのすばらしい機能を提供します。

  • オブジェクトのリストはページ送りになる
  • リストをソート可能
  • リストをフィルタリングできる
  • オブジェクトを作成編集削除できる
  • 選択されたオブジェクトを一括で削除できる
  • フォームのバリデーションができる
  • ユーザーはフラッシュメッセージによってすぐに結果を得る
  • さらに多くの機能・・・

アドミンジェネレータはパッケージを設定し、簡単にバックエンドインターフェイスを作るために必要なすべての機能を提供します。

2つの生成されたモジュールを見ると、ウェブデザインされていないことにお気づきになるでしょうが、symfony 組み込みのアドミンジェネレータにはデフォルトで基本的なグラフィックインターフェイスがあります。

現在、sfDoctrinePlugin からのアセットが web/ フォルダに置かれていません。

plugin:publish-assets によって web/ フォルダの下に配置します。

$ php symfony plugin:publish-assets

ユーザーエクスペリエンスを少し改善するために、デフォルトのバックエンドをカスタマイズする必要があります。異なるモジュールの間を移動することを楽にするためにシンプルなメニューを追加します。

デフォルトのレイアウトファイルの内容を次のコードに置き換えます。

// 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="/legacy/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="/legacy/images/jobeet-mini.png" />
        powered by <a href="/">
        <img src="/legacy/images/symfony.gif" alt="symfony framework" /></a>
      </div>
    </div>
  </body>
</html>

レイアウトは admin.css スタイルシートを使います。これは4日目の間に他のスタイルシートと一緒にインストールしたので、このファイルは web/css/ のなかに存在しなければなりません。

アドミンジェネレータの外観

最後に、routing.yml ファイルのデフォルトの homepage を変更します。

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

symfony のキャッシュ

好奇心旺盛であれば、apps/backend/modules/ ディレクトリの下でタスクによって生成されたファイルをすでに開いているかもしれません。そうでなければ、今ファイルを開いてください。驚くべきことに、templates ディレクトリは空で、actions.class.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/ ディレクトリにあり、「本当の」モジュールを格納しています。

// 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 はモデルとフォームクラスを生成します。アドミンジェネレータの場合、生成されたモジュールを module フォルダ内の config/generator.yml ファイルを編集することで設定できます。

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

generator.yml ファイルを更新するたびに、symfony はキャッシュを再生成します。後で見るように、アドミンジェネレータに生成されたモジュールをカスタマイズするのは簡単で、早く、楽しいことです。

note

キャッシュファイルの自動再生成は開発環境でのみ行われます。運用環境では、cache:clear タスクを使ってキャッシュを手動でクリアする必要があります。

NOTE with_show パラメータに効果はありません。このパラメータは doctrine:generate-module タスクを用いた 「通常の」モジュールのときにのみ効果があります。

バックエンドのコンフィギュレーション

admin モジュールは generator.yml ファイルの config キーを編集することでカスタマイズできます。コンフィギュレーションは7つのセクションにわかれています。

  • actions: リストとフォームでのアクションのデフォルトコンフィギュレーション
  • fields: フィールドのデフォルトコンフィギュレーション
  • list: リストのコンフィギュレーション
  • filter: フィルタのコンフィギュレーション
  • form: 新規/編集フォームのコンフィギュレーション
  • edit: 編集ページ固有のコンフィギュレーション
  • new: 新規作成ページ固有のコンフィギュレーション

カスタマイズを始めましょう。

タイトルのコンフィギュレーション

category モジュールの listeditnew セクションのタイトルは 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 は動的な値を含みます: %% で囲まれるすべての文字列は対応するオブジェクトのカラムの値に置き換わります。

タイトル

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

フィールドのコンフィギュレーション

異なるビュー (listnewedit) はフィールドで構成されています。フィールドはモデルクラスのカラムもしくは後で見るバーチャルカラムになります。

デフォルトのフィールドコンフィギュレーションは 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?, help: Whether the job can also be published on affiliate websites, or not }

フィールドのコンフィギュレーション

fields セクションはすべてのビューのフィールドコンフィギュレーションをオーバーライドします。つまり、 is_activated フィールドのラベルが listeditnew ビューに合わせて変化するということです。

アドミンジェネレータのコンフィギュレーションはコンフィギュレーションカスケードの原則に基づきます。たとえば、ラベル用の list ビューのみを変更したい場合、list セクションの下の fields オプションを定義します。

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

メインの fields セクションの下で設定されるコンフィギュレーションはビュー固有のコンフィギュレーションによってオーバーライドされます。オーバーライドのルールは次のとおりです:

  • neweditfields を継承する form を 継承する
  • listfields を継承する
  • filterfields を継承する

note

フォームセクション (formeditnew) に関しては、labelhelp オプションはフォームクラスで定義されるものをオーバーライドします。

list ビューのコンフィギュレーション

display

デフォルトでは、list ビューのカラムはすべてスキーマファイルの通りであるモデルのカラムです。表示されるカラムの順序を定義すると display オプションはデフォルトをオーバーライドします。

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

name カラムの前の = 記号は文字列をリンクに変換するための慣習です。

テーブルのリスト

読みやすくするために job モジュールに同じことをやってみましょう:

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

layout

list は異なるレイアウトで表示されます。デフォルトのレイアウトはテーブルレイアウト です。このことはそれぞれのカラムの値がそれぞれのテーブルカラムのなかにあることを意味します。しかし job モジュールに関しては、他の組み込みのレイアウトであるスタックレイアウトを使うほうが良いでしょう。:

# 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%%)

スタックレイアウトでは、それぞれのオブジェクトはparams オプションによって定義された通りに1つの文で表示されます。

note

display オプションはユーザーがソートできるようにするためにまだ必要です。

「バーチャル」カラム

この設定によって、%%category_id%% セグメントはカテゴリの主キーによって置き換えられます。しかし、カテゴリの名前を表示するほうがよりわかりやすくなるでしょう。

%% の表記を使うときは、変数はデータベーススキーマの実際にカラムに一致する必要はありません。アドミンジェネレータはモデルクラスに関連する getter が見つかることだけを必要とします。

カテゴリの名前を表示するために、JobeetJob モデルクラスの getCategoryName() メソッドを定義し %%category_id%%%%category_name%% で置換します。

しかし JobeetJob クラスにはすでに関連カテゴリオブジェクトを返す getJobeetCategory() メソッドがあります。%%jobeet_category%% を使う場合、JobeetCategory クラスにはオブジェクトを文字列に変換する __toString() マジックメソッドがあるので、%%jobeet_category%% は動作します。

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

スタックレイアウト

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

max_per_page

batch_actions

リストにおいて、いくつかのオブジェクト上でアクションを実行できます。category モジュールのこれらのバッチは必要ありませんので削除しましょう。

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

バッチアクションを削除する

batch_actions オプションはバッチアクションのリストを定義します。空の配列を指定すれば機能を削除できます。

デフォルトでは、それぞれのモジュールにはフレームワークによって定義されている delete バッチアクションが用意されていますが、job モジュールの場合に、選択された求人の有効期間を30日延長することが必要な場合を考えてみましょう。

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

すべての_ で始まるアクションはフレームワークによって提供される組み込みのアクションです。ブラウザをリフレッシュして期限延長のバッチアクションを選ぶと、symfony は executeBatchExtend() メソッドを作ることを指示する例外を投げます。

// apps/backend/modules/job/actions/actions.class.php
class jobActions extends autoJobActions
{
  public function executeBatchExtend(sfWebRequest $request)
  {
    $ids = $request->getParameter('ids');
 
    $q = Doctrine_Query::create()
      ->from('JobeetJob j')
      ->whereIn('j.id', $ids);
 
    foreach ($q->execute() as $job)
    {
      $job->extend(true);
    }
 
    $this->getUser()->setFlash('notice', 'The selected jobs have been extended successfully.');
 
    $this->redirect('jobeet_job');
  }
}

選択されたプライマリキーは ids リクエストパラメータに保存されます。それぞれ選択された求人で、期限切れのチェックをバイパスするめの追加の引数と共に JobeetJob::extend() メソッドが呼び出されます。

この新しい引数を考慮して extend() メソッドを更新しましょう。

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

すべての求人の有効期間が延長された後で、ユーザーは job モジュールのホームページにリダイレクトされます。

カスタムのバッチアクション

object_actions

list には、単独のオブジェクトで実行できるアクション用の追加カラムがあります。category モジュールで、カテゴリの名前には編集するためのリンクがあり、リストから直接削除できる必要がないので、これらを削除しましょう。

# 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() アクションを定義する必要があります。

// 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');
  }
 
  // ...
}

カスタムのオブジェクトアクション

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 アクションを作りましょう。

// apps/backend/modules/job/actions/actions.class.php
class jobActions extends autoJobActions
{
  public function executeListDeleteNeverActivated(sfWebRequest $request)
  {
    $nb = Doctrine_Core::getTable('JobeetJob')->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');
  }
 
  // ...
}

昨日定義した JobeetJobTable::cleanup() メソッドを再利用しました。 これも MVC パターンによって提供される再利用性のすばらしい例です。

note

action パラメータを渡すことで実行されるアクションも変更できます。

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

アクション

tablemethod

Web デバッグツールバーによって表示されるように、求人リストのページを表示するために必要なデータベースへのリクエスト回数は14回です。

その数字をクリックすると、ほとんどのリクエストがそれぞれの求人のカテゴリ名を取得するためだということがわかるでしょう。

修正前のリクエスト回数

リクエストの数を減らすために、tablemethod オプションを使用して求人情報を得るのに使われるデフォルトメソッドを変更できます。

# apps/backend/modules/job/config/generator.yml
config:
  list:
    table_method: retrieveBackendJobList

lib/model/doctrine/JobeetJobTable.class.php に設置されている JobeetJobTable クラスの retrieveBackendJobList メソッドを作らなければなりません。

// lib/model/doctrine/JobeetJobTable.class.php
class JobeetJobTable extends Doctrine_Table
{
  public function retrieveBackendJobList(Doctrine_Query $q)
  {
    $rootAlias = $q->getRootAlias();
 
    $q->leftJoin($rootAlias . '.JobeetCategory c');
 
    return $q;
  }
 
  // ...

retrieveBackendJobList() メソッドは jobcategory テーブル間の JOIN を追加し、それぞれの求人に関連するカテゴリオブジェクトを自動的に作ります。

これでリクエストの回数は4回に減ります。

修正後のリクエスト回数

フォームビューのコンフィギュレーション

フォームビューは3つのセクション: formeditnewで構成されます。これらすべては同じ設定機能をもち、form セクションは editnew セクションのフォールバックとしてのみ存在します。

display

リストと同じように、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つのグループ (ContentAdmin) が定義され、それぞれはフォームフィールドのサブセットを含みます。

フィールドのグルーピング

note

Admin グループのカラムはまだブラウザには表示されません。これらは求人フォームの定義に設定されていないからです。admin アプリケーション用にカスタムの求人フォームクラスを定義するときのいくつかのセクションにあらわれます。

アドミンジェネレータは多対多のリレーションシップ用の組み込み機能をサポートします。カテゴリフォームにおいて、名前の入力ボックス、スラッグの入力ボックス、関連するアフィリエイト用のドロップダウンボックスがあります。このページでこのリレーションを編集するのは意味がないので、削除しましょう。

// lib/form/doctrine/JobeetCategoryForm.class.php
class JobeetCategoryForm extends BaseJobeetCategoryForm
{
  public function configure()
  {
    unset($this['created_at'], $this['updated_at'], $this['jobeet_affiliates_list']);
  }
}

「バーチャル」カラム

求人フォームの display オプションにおいて、_generated_token フィールドはアンダースコア (_) で始まります。これはこのフィールドの表示が _generated_token.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() メソッドを通してアクセスできます。

note

チルダ (~) をフィールドの名前のプレフィックスにすることで、表示をコンポーネントに委譲することもできます。

class

管理者によって使われるフォームとして、ユーザーの求人フォームよりも多くの情報を表示してきました。しかし今では、JobeetJobForm クラスでこれらを削除したので、これらのなかにはフォームに表示されないものがあります。

フロントエンドとバックエンドで異なるフォームを用意するには、2つのフォームクラスを作る必要があります。JobeetJobForm クラスを継承する BackendJobeetJobForm クラスを作りましょう。隠したフィールドが同じでないようにするため、BackendJobeetJobForm でオーバーライドするメソッドのなかの unset() 関数を移動させるように少しリファクタリングする必要があります。

// lib/form/doctrine/JobeetJobForm.class.php
class JobeetJobForm extends BaseJobeetJobForm
{
  public function configure()
  {
    $this->removeFields();
 
    $this->validatorSchema['email'] = new sfValidatorAnd(array(
      $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/doctrine/BackendJobeetJobForm.class.php
class BackendJobeetJobForm extends JobeetJobForm
{
  protected function removeFields()
  {
    unset(
      $this['created_at'], $this['updated_at'],
      $this['token']
    );
  }
}

アドミンジェネレータによって使われるデフォルトのフォームクラスは class オプションを設定することでオーバーライドできます。

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

note

クラスを新たに作ったので、キャッシュをクリアすることをお忘れなく。

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

// lib/form/doctrine/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: true の場合、ファイルは画像としてレンダリングされる
  • edit_mode: フォームが編集モードかそうではないか
  • with_delete: 削除用のチェックボックスを表示するか
  • template: ウィジェットをレンダリングするために使うテンプレート

ファイルのアップロード

tip

生成されたテンプレートは多くの classid 属性を定義するので、アドミンジェネレータの見た目はとてもかんたんに調整できます。たとえば、ロゴフィールドは sf_admin_form_field_logo クラスを使ってカスタマイズできます。それぞれのフィールドは sf_admin_text もしくは sf_admin_boolean のようなフィールドタイプによって決まるクラスも持ちます。

edit_mode オプションは sfDoctrineRecord::isNew() メソッドを使います。

これはフォームのモデルオブジェクトが新しい場合は true を返し、そうでなければ false を返します。埋め込みオブジェクトのステータスによって異なるウィジェットもしくはバリデータを用意する必要がある場合にこれはとても役立ちます。

フィルタのコンフィギュレーション

フィルタの設定方法はフォームビューの設定方法とまったく同じです。実際、フィルタは単なるフォームです。 そしてフォームと同じように、クラスは doctrine:build --all タスクで生成されました。doctrine:build --filters タスクでこれらを再生成することもできます。

フォームフィルタクラスは lib/filter/ ディレクトリの下に配置され、それぞれのモデルクラスに関連したフィルタフォームクラスが用意されています (JobeetJobForm に対応するのは JobeetJobFormFilter)。

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]

フィルタは常にオプションなので、フィールドが表示されるように設定するために、フィルタフォームクラスをオーバーライドする必要はありません。

フィルタ

アクションのカスタマイズ

設定が十分ではないとき、これまでの機能の拡張方法で見てきたように新しいメソッドをアクションクラスに追加できます。生成されたアクションモジュールをオーバーライドすることもできます。

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

それぞれの生成メソッドは1つのことしか行わないので、たくさんのコードをコピー&ペーストしなくてもふるまいを変更するのはかんたんです。

テンプレートのカスタマイズ

アドミンジェネレータによって HTML コードに追加される classid 属性を用いて生成されたテンプレートをカスタマイズする方法を見てきました。

クラスと同じように、オリジナルのテンプレートをオーバーライドすることもできます。テンプレートはプレーンな PHP ファイルで PHP クラスではないので、モジュールのなかで同じ名前のテンプレートを作ることでテンプレートをオーバーライドできます (たとえば job アドミンモジュールでは apps/backend/modules/job/templates/ ディレクトリ)。

テンプレート 説明
_assets.php テンプレート用の CSS と JS を表示する
_filters.php フィルタボックスを表示する
_filters_field.php 単独のフィルタフィールドを表示する
_flashes.php フラッシュメッセージを表示する
_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 行単位のスタックレイアウトを表示する
_list_td_tabular.php リストの単独のフィールドを表示する
_list_th_stacked.php ヘッダー用の単独のカラム名を表示する
_list_th_tabular.php ヘッダー用の単独のカラム名を表示する
_pagination.php リストのページネーションを表示する
editSuccess.php edit ビューを表示する
indexSuccess.php list ビューを表示する
newSuccess.php new ビューを表示する

最終的なコンフィギュレーション

Jobeet の admin の最終的なコンフィギュレーションは次のとおりです。

# apps/backend/modules/job/config/generator.yml
generator:
  class: sfDoctrineGenerator
  param:
    model_class:           JobeetJob
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              ~
    plural:                ~
    route_prefix:          jobeet_job
 
    with_doctrine_route:   true
    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>%%JobeetCategory%%</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 }
        table_method: retrieveBackendJobList
      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: sfDoctrineGenerator
  param:
    model_class:           JobeetCategory
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              ~
    plural:                ~
    route_prefix:          jobeet_category
 
    with_doctrine_route:   true
    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 用のすばらしいバックエンドインターフェイスを短時間で開発しました。

tip

何かが YAML ファイルで設定可能であるとき、プレーンな PHP コードを使うことができるのはご存じのとおりです。アドミンジェネレータの場合、apps/backend/modules/job/lib/jobGeneratorConfiguration.class.php ファイルを編集できます。これによって YAML ファイルと同じオプションが PHP で提供されます。メソッドの名前を学ぶには、生成された基底クラスの cache/backend/dev/modules/autoJob/lib/BaseJobGeneratorConfiguration.class.php をご覧ください。

また明日

丁度一時間で、Jobeet プロジェクト用に十分な機能をもつバックエンドインターフェイスを開発しました。全体で、50行に満たない PHP コードしか書きませんでした。こんなにたくさんの機能があるわりにまずますの成果です!

明日は、ユーザー名とパスワードでバックエンドアプリケーションをセキュアにする方法を見ることになります。symfony のユーザークラスに関して話す機会でもあります。