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つのコマンドは JobeetJob と JobeetCategory モデルクラスに対してそれぞれ job と category モジュールを生成します。
--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 モジュールの 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 は動的な値を含みます: %% で囲まれるすべての文字列は対応するオブジェクトのカラムの値に置き換わります。

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) はフィールドで構成されています。フィールドはモデルクラスのカラムもしくは後で見るバーチャルカラムになります。
デフォルトのフィールドコンフィギュレーションは 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 フィールドのラベルが 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はfieldsを継承するformを 継承するlistはfieldsを継承するfilterはfieldsを継承する
note
フォームセクション (form、edit と new) に関しては、label と help オプションはフォームクラスで定義されるものをオーバーライドします。
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

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() メソッドは job と category テーブル間の JOIN を追加し、それぞれの求人に関連するカテゴリオブジェクトを自動的に作ります。
これでリクエストの回数は4回に減ります。

フォームビューのコンフィギュレーション
フォームビューは3つのセクション: form、edit と newで構成されます。これらすべては同じ設定機能をもち、form セクションは edit と new セクションのフォールバックとしてのみ存在します。
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つのグループ (Content と Admin) が定義され、それぞれはフォームフィールドのサブセットを含みます。

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
生成されたテンプレートは多くの class と id 属性を定義するので、アドミンジェネレータの見た目はとてもかんたんに調整できます。たとえば、ロゴフィールドは 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 コードに追加される class と id 属性を用いて生成されたテンプレートをカスタマイズする方法を見てきました。
クラスと同じように、オリジナルのテンプレートをオーバーライドすることもできます。テンプレートはプレーンな 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 のユーザークラスに関して話す機会でもあります。
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.