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

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 を作り、その後はアプリケーションの名前を使います。

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

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

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

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

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

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

バックエンドモジュール

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

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

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 になります)。

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

[yml]
# apps/backend/config/routing.yml
jobeet_job:

class: sfDoctrineRouteCollection class: sfPropelRouteCollection options: model: JobeetJob module: job prefixpath: job column: id withwildcard_routes: true

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

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

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

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

$ php symfony help propel:generate-admin

このコマンドによって他のソフトウェアの典型的な使い方の例と同じようにタスクのすべての引数とオプションが表示されます。 TIP いつものことですが、新しいタスクを使う前にヘルプを読むのはよい考えです。

$ php symfony help doctrine:generate-admin

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

バックエンドの外観

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

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

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

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

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

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

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

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

$ php symfony plugin:publish-assets

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

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

[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/ のなかに存在しなければなりません。

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

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

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

symfony のキャッシュ

好奇心旺盛であれば、apps/backend/modules/ ディレクトリの下でタスクによって生成されたファイルをすでに開いているかもしれません。そうでなければ、今ファイルを開いてください。驚くべきことに、templates ディレクトリは空で、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 はモデルとフォームクラスを生成します。アドミンジェネレータの場合、生成されたモジュールを module フォルダ内の config/generator.yml ファイルを編集することで設定できます。

[yml]
# apps/backend/modules/job/config/generator.yml
generator:

class: sfPropelGenerator class: sfDoctrineGenerator param: modelclass: JobeetJob theme: admin nonverbosetemplates: true withshow: false singular: ~ plural: ~ routeprefix: jobeetjob withpropelroute: true

    config:
      actions: ~
      fields:  ~
      list:    ~
      filter:  ~
      form:    ~
      edit:    ~
      new:     ~

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

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

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

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

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

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

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

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

category モジュールの listeditnew セクションのタイトルは title オプションを定義することでカスタマイズできます。

[yml]
# 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 モジュールのコンフィギュレーションの場合もとてもよく似ています。

[yml]
# 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 セクションでカスタマイズできます。

[yml]
# 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 オプションを定義します。

[yml]
# 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 オプションはデフォルトをオーバーライドします。

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

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

テーブルのリスト

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

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

layout

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

[yml]
# 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%% は動作します。

[yml]
# 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 オプションを追加することで、デフォルトのソートカラムを設定できます。

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

max_per_page

デフォルトでは、リストはページ送りになり、それぞれのページには20のアイテムが表示されます。これは max_per_page オプションで変更できます。

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

max<em>per</em>page

batch_actions

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

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

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

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

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

[yml]
# apps/backend/modules/job/config/generator.yml
config:
  list:
    batch_actions:
      _delete:    ~
      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)

$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() メソッドを更新しましょう。

[php]

// lib/model/JobeetJob.php // lib/model/doctrine/JobeetJob.class.php class JobeetJob extends BaseJobeetJob { public function extend($force = false) { if (!$force && !$this->expiresSoon()) { return false; }

$this->setExpiresAt(time() + 86400 * sfConfig::get('appactivedays')); $this->setExpiresAt(date('Y-m-d', time() + 86400 * sfConfig::get('appactivedays'))); $this->save();

    return true;
  }

  // ...
}

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

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

object_actions

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

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

job モジュールに関して、既存のアクションを維持して、バッチアクションとして追加したものに似た新しい extend アクションを追加しましょう。

[yml]
# 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');
  }

  // ...
}

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

actions

オブジェクトのリストもしくは単独のオブジェクトにアクションをリンクする方法を見てきました。新しいオブジェクトを作るように、actions オプションはオブジェクトをまったく取らないアクションを定義します。デフォルトの new アクションを削除して60日以上投稿者によってアクティベートされなかったすべての求人を削除する新しいアクションを追加しましょう。

[yml]
# 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); $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');
  }

  // ...
}

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

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

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

アクション

peermethod

tablemethod

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

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

修正前のリクエスト回数

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

[yml]
# apps/backend/modules/job/config/generator.yml
config:
  list:

peer_method: doSelectJoinJobeetCategory

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

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

[php]
// 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 オプションはフィールドをグループに編集するためにも使うことができます。

[yml]
# 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 アプリケーション用にカスタムの求人フォームクラスを定義するときのいくつかのセクションにあらわれます。

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

[php]

// lib/form/JobeetCategoryForm.class.php // lib/form/doctrine/JobeetCategoryForm.class.php class JobeetCategoryForm extends BaseJobeetCategoryForm { public function configure() { unset($this['jobeetcategoryaffiliatelist']); unset($this['createdat'], $this['updatedat'], $this['jobeetaffiliates_list']); } }

「バーチャル」カラム

求人フォームの 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() メソッドを通してアクセスできます。

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

class

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

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

[php]

// lib/form/JobeetJobForm.class.php // 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/BackendJobeetJobForm.class.php // lib/form/doctrine/BackendJobeetJobForm.class.php class BackendJobeetJobForm extends JobeetJobForm { protected function removeFields() { unset( $this['createdat'], $this['updatedat'], $this['token'] ); } }

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

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

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

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

[php]

// lib/form/BackendJobeetJobForm.class.php // 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 オプションは sfPropelRecord::isNew() メソッドを使います。 edit_mode オプションは sfDoctrineRecord::isNew() メソッドを使います。

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

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

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

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

category モジュールのためにこれらを完全に削除しましょう。

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

job モジュールではこれらの一部を削除しましょう。

[yml]
# 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 の最終的なコンフィギュレーションは次のとおりです。

[yml]
# apps/backend/modules/job/config/generator.yml
generator:

class: sfDoctrineGenerator class: sfPropelGenerator param: modelclass: JobeetJob theme: admin nonverbosetemplates: true withshow: false singular: ~ plural: ~ routeprefix: jobeetjob

withpropelroute: true withdoctrineroute: true config: actions: ~ fields: isactivated: { label: Activated?, help: Whether the user has activated the job, or not } ispublic: { label: Public? } list: title: Job Management layout: stacked display: [company, position, location, url, isactivated, email] params: | %%isactivated%% %%jobeetcategory%% - %%company%% %%isactivated%% %%JobeetCategory%% - %%company%% (%%email%%) is looking for a %%=position%% (%%location%%) maxperpage: 10 sort: [expiresat, desc] batchactions: delete: ~ extend: ~ objectactions: extend: ~ edit: ~ _delete: ~ actions: deleteNeverActivated: { label: Delete never activated jobs } peermethod: doSelectJoinJobeetCategory tablemethod: retrieveBackendJobList filter: display: [categoryid, company, position, description, isactivated, ispublic, email, expiresat] form: class: BackendJobeetJobForm display: Content: [categoryid, type, company, logo, url, position, location, description, howtoapply, ispublic, email] Admin: [generatedtoken, isactivated, 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 class: sfDoctrineGenerator param: modelclass: JobeetCategory theme: admin nonverbosetemplates: true withshow: false singular: ~ plural: ~ routeprefix: jobeetcategory

withpropelroute: true withdoctrineroute: true config: actions: ~ fields: ~ list: title: Category Management display: [=name, slug] batchactions: {} objectactions: {} 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 のユーザークラスに関して話す機会でもあります。

ORM