Caution: You are browsing the legacy symfony 1.x part of this website.

12日目: Adminジェネレーター

1.2 / Doctrine

昨日Jobeetに追加した機能のおかげで フロントエンドアプリケーションは求職者と仕事の提供者が十分に利用できます。 バックエンドアプリケーションを少し話すことにします。

今日は、symfonyのadminジェネレーターのおかげで、Jobeet用の完全なバックエンドインターフェイスを1時間で開発します。

バックエンドの作成

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

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

バックエンドアプリケーションを使うのがJobeetの管理者だけであっても、symfonyのすべての組み込み機能を有効にしました。

tip

パスワードでドル記号($)のような特殊記号を使いたい場合、コマンドラインで適切にエスケープする必要があります:

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

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

note

フロントエンドアプリケーションを作成したとき、運用環境のフロントコントローラーの名前はindex.phpでした。 ディレクトリごとに1つのindex.phpファイルだけ用意できるので、symfonyは初期の運用環境のフロントコントローラーであるindex.phpファイルを作り名前をアプリケーションからつけます。

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

しかし8日目でみたように、コンフィギュレーションは異なるレベルで設定できます。 apps/frontend/config/app.ymlファイルの内容をconfig/app.ymlに移動させることで、すべてのアプリケーションの間で設定は共有され問題は修正されます。 adminジェネレーターでモデルクラスを広範囲で使うので、今変更をします。 バックエンドアプリケーションの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 --module=category

これら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

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

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

  • prefix_path: 生成されるルートのプレフィックスのパスを定義する(たとえば、編集ページは/job/1/editのようになる)。
  • column: オブジェクトを参照するリンク用のURLで使用するテーブルのカラムを定義する
  • with_wildcard_routes: adminインターフェイスは古典的なCRUDオペレーション以上の機能を持ち、このオプションによってルートを編集せずに、オブジェクトとコレクションアクションを定義できます。

tip

As always, 新しいタスクを使う前にヘルプを読むのはよい考えです。

$ php symfony help doctrine:generate-admin

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

バックエンドの外見

生成されたモジュールは直ちに利用できます:

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

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

  • オブジェクトのリストはページ分割される
  • リストはソート可能である
  • リストはフィルタリング可能である
  • オブジェクトは作成編集削除が可能である
  • 選択されたオブジェクトはバッチで削除可能である
  • フォームのバリデーションが有効である
  • flashメッセージによってユーザーに即座のフィードバックが行われる
  • さらに多くの機能がある

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

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

デフォルトの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="/legacy/images/logo.jpg" alt="Jobeet Job Board" />
          </a>
        </h1>
      </div>
 
      <div id="menu">
        <ul>
          <li>
            <?php echo link_to('Jobs', '@jobeet_job_job') ?>
          </li>
          <li>
            <?php echo link_to('Categories', '@jobeet_category_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/の中に存在しなければなりません。

adminジェネレーターの外見

最終的に、routing.ymlでデフォルトのホームページを変更します:

# 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())
    ))
    {
 
// ...

adminジェネレーターの動作は既知のふるまいを思い出させてくれます。 実際、これはモデルとフォームクラスに関してすでに学んだこととよく似ています。 モデルスキーマの定義に基づいて、symfonyはモデルとフォームクラスを生成します。 adminジェネレーターに関して、モジュールで見つかる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_doctrine_route:   1
 
    config:
      actions: ~
      fields:  ~
      list:    ~
      filter:  ~
      form:    ~
      edit:    ~
      new:     ~

generator.ymlファイルを更新するたびに、symfonyはキャッシュを再生成します。 今日見たように、adminジェネレーターで生成したモジュールのカスタマイズは簡単で、速く、面白いです。

note

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

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

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ビュー用に変更されることを意味します。

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

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

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

  • neweditformを継承し、formfieldsを継承する
  • 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は異なるレイアウトで表示されます。 デフォルトでは、レイアウトは~tabular|テーブルレイアウト~です。 このことはそれぞれのカラムの値が独自のテーブルカラムの中にあることを意味します。 しかし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オプションによって定義されます。

note

displayオプションはこれはユーザーによってソートできるカラムを定義するのでまだ必要です。

"バーチャル"カラム

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

%%の表記を使うときは、変数はデータベーススキーマの実際にカラムに対応する必要はありません。 adminジェネレーターはモデルクラスの関連ゲッターを見つけることだけが必要です。

カテゴリの名前を表示するには、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%%)

stackedレイアウト

sort

管理者として、投稿された最新の求人を見ることにご興味があるでしょう。 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

ページごとの最大数

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

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

note

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

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

アクション

table_method

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

その数字をクリックすると、それぞれの求人に対してカテゴリの名前を読み取るための最新のリクエスト回数がわかります:

修正前のリクエスト回数

リクエストの回数を減らすには、 table_methodオプションを指定することで求人を取得するために使われるデフォルトのメソッドを変更できます:

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

lib/model/doctrine/JobeetJobTable.class.phpに設置されるJobeetJobTableretrieveBackendJobListメソッドを作らなければなりません。

// 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アプリケーション用にカスタムの求人フォームクラスを定義するときにこれらはわずかなセクションに現れます。

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クラスを作りましょう。 同じ隠しフィールドを持たないので、unset()ステートメントをBackendJobeetJobFormでオーバーライドされるメソッドに移動させるために、JobeetJobFormクラスを少しリファクタリングすることも必要です:

// 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
{
  public function configure()
  {
    parent::configure();
  }
 
  protected function removeFields()
  {
    unset(
      $this['created_at'], $this['updated_at'],
      $this['token']
    );
  }
}

adminジェネレーターによって使われるデフォルトのフォームクラスは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属性を定義するので、adminジェネレーターの見た目はとても簡単に調整できます。 たとえば、ロゴフィールドは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つのことしか行わないので、たくさんのコードをコピー&ペーストしなくてもふるまいを変更するのは簡単です。

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

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

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

テンプレート 説明
_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ビューを表示する

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

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:   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>%%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:   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用のすばらしいバックエンドインターフェイスを短時間で開発しました。

tip

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

また明日

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

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