Webプロジェクトにおいて、たいていのフォームはモデルオブジェクトを作るもしくは修正するために使われます。通常これらのオブジェクトはORMのおかげでシリアライズ化されます。symfonyのフォームシステムはsymfonyに組み込まれたORMであるDoctrineと橋渡しをするための追加レイヤーを提供し、これらのモデルオブジェクトに基づくフォームの実装を簡単にします。
この章ではフォームをDoctrineのオブジェクトモデルに統合する方法を詳しく説明します。Doctrineをsymfonyに統合して使うことにすでに慣れているのであれば大いにお勧めします。そうでなければ、"The Definitive Guide to symfony"のモデルレイヤーの内側の章を参照してください。
始める前に
この章において、記事の管理システムを作ります。データベースのスキーマから始めてみましょう。これはリスト4-1で示されるような5つのテーブル:article
、author
、category
、tag
、とarticle_tag
で構成されます。
リスト4-1 - データベーススキーマ
// config/doctrine/schema.yml Article: actAs: [Sluggable, Timestampable] columns: title: type: string(255) notnull: true content: type: clob status: string(255) author_id: integer category_id: integer published_at: timestamp relations: Author: foreignAlias: Articles Category: foreignAlias: Articles Tags: class: Tag refClass: ArticleTag foreignAlias: Articles Author: columns: first_name: string(20) last_name: string(20) email: string(255) active: boolean Category: columns: name: string(255) Tag: columns: name: string(255) ArticleTag: columns: article_id: type: integer primary: true tag_id: type: integer primary: true relations: Article: onDelete: CASCADE Tag: onDelete: CASCADE
テーブル間のリレーションは次の通りです:
article
とauthor
テーブルは一対多のリレーション: 記事は1人の著者によってのみ書かれるarticle
とcategory
テーブルは一対多のリレーション: 記事は1つのカテゴリに所属するもしくはまったく所属しないarticle
とtag
テーブル間の多対多のリレーション
フォームクラスを生成する
article
、author
、category
、とtag
テーブルの情報を編集することを考えます。これを行うには、これらのそれぞれのテーブルにリンクされたフォームを作りデータベースのスキーマに関連するウィジェットとスキーマを設定する必要があります。これらのフォームをマニュアル通りに作ることは可能ですが、長く退屈なタスクで、結局の所、複数のファイルにおいて同じ種類の情報の反復が強制させられます(カラムとフィールド名、カラムとフィールドの最大サイズ・・・)。さらに、モデルを変更するたびに、関連するフォームクラスも変更しなければなりません。幸いにして、Doctrineプラグインに組み込まれているdoctrine:build-forms
タスクはオブジェクトモデルに関連するフォームを生成するこの作業を自動化します:
$ ./symfony doctrine:build-forms
フォーム生成の間に、タスクはモデルのイントロスペクションを利用しテーブル間のリレーションを考慮してテーブルごとにそれぞれのカラム用のバリデータとウィジェットを持つ1つのクラスを作ります。
note
doctrine:build-all
とdoctrine:build-all-load
はdoctrine:build-forms
タスクを自動的に起動させてフォームクラスも更新します。
これらのタスクを実行した後で、ファイル構造がlib/form/
ディレクトリの中に作られます。上記の例のスキーマのために作られたファイルは次の通りです:
lib/ form/ doctrine/ ArticleForm.class.php ArticleTagForm.class.php AuthorForm.class.php CategoryForm.class.php TagForm.class.php base/ BaseArticleForm.class.php BaseArticleTagForm.class.php BaseAuthorForm.class.php BaseCategoryForm.class.php BaseFormDoctrine.class.php BaseTagForm.class.php
doctrine:build-forms
タスクはスキーマのそれぞれのテーブル用の2つのクラスを生成し、1つの基底クラスはlib/form/base
ディレクトリに、もう1つの基底クラスはlib/form/
ディレクトリです。例えば、author
テーブルはBaseAuthorForm
とAuthorForm
クラスで構成され、これらのクラスはlib/form/base/BaseAuthorForm.class.php
とlib/form/AuthorForm.class.php
ファイルに生成されました。
下記のテーブルはAuthorForm
フォームの定義に関連する異なったクラス間の階層をまとめています。
クラス | パッケージ | 対象 | 説明 |
---|---|---|---|
AuthorForm | project | developer | 生成フォームをオーバーライドする |
BaseAuthorForm | project | symfony | スキーマに基づきdoctrine:build-forms タスクの実行ごとにオーバーライドされる |
BaseFormDoctrine | project | developer | Doctrineフォームのグローバルなカスタマイズを許可する |
sfFormDoctrine | Doctrine plugin | symfony | Doctrineフォームの基底クラス |
sfForm | symfony | symfony | symfonyフォームの基底クラス |
Author
クラスからオブジェクトを作成もしくは編集するために、リスト4-2で説明されたAuthorForm
クラスを使います。お気づきのように、このクラスは設定を通して生成されたBaseAuthorForm
を継承していますが、メソッドを含みません。AuthorForm
クラスは設定をカスタマイズして上書きするために使うクラスです。
リスト4-2 - AuthorForm
クラス
class AuthorForm extends BaseAuthorForm { public function configure() { } }
リスト4-3はauthor
テーブル用のモデルをイントロスペクトして生成されたバリデータとウィジェットを持つBaseAuthorForm
クラスを示しています。
リスト4-3 - author
テーブルのためのフォームを表すBaseAuthorForm
クラス
class BaseAuthorForm extends BaseFormDoctrine { public function setup() { $this->setWidgets(array( 'id' => new sfWidgetFormInputHidden(), 'first_name' => new sfWidgetFormInput(), 'last_name' => new sfWidgetFormInput(), 'email' => new sfWidgetFormInput(), )); $this->setValidators(array( 'id' => new sfValidatorDoctrineChoice(array('model' => 'Author', 'column' => 'id', 'required' => false)), 'first_name' => new sfValidatorString(array('max_length' => 20, 'required' => false)), 'last_name' => new sfValidatorString(array('max_length' => 20, 'required' => false)), 'email' => new sfValidatorString(array('max_length' => 255)), )); $this->widgetSchema->setNameFormat('author[%s]'); $this->errorSchema = new sfValidatorErrorSchema($this->validatorSchema); parent::setup(); } public function getModelName() { return 'Author'; } }
生成クラスはわずかな違いを除いて、以前の章ですでに作成されたフォームによく似ています:
- 基底クラスは
sfForm
の代わりにBaseFormDoctrine
- バリデータとウィジェットの設定は
configure()
メソッドの代わりに、setup()
メソッドによって行われる getModelName()
メソッドはこのフォームに関連するDoctrineクラスを返す
tip
基底クラスは設定のためにconfigure(メソッドの代わりに
setup()メソッドを使います。これによって開発者は
parent::configure()`の呼び出しに対処せずに空の生成クラスの設定を上書きできます。
フォームのフィールド名はスキーマ: id
、first_name
、last_name
、とemail
に設定するカラム名に対して理想的なものです。
author
テーブルのそれぞれのカラムに対して、doctrine:build-forms
タスクはスキーマの定義に従ってウィジェットとバリデータを生成します。タスクは可能な限り常に最も安全なバリデータを生成します。id
フィールドを考えてみましょう。値が有効な整数であるかチェックできます。代わりにここで生成されたバリデータによって(既存のオブジェクトを編集するために)識別子が実際に存在するもしくは(新しいオブジェクトを作ることができるように)識別子が空であることをバリデートできます。これはより強いバリデーションです。
生成フォームは即座に使うことができます。<?php echo $form ?>
ステートメントを追加すれば、コードを一行も書かずにバリデーション機能を持つフォームを作ることができます。
プロトタイプを素早く作る機能を越えて、生成クラスを修正しなくても生成フォームを拡張するのは簡単です。これは基底とフォームクラスの継承システムのおかげです。
最後にデータベースのスキーマを拡張するたびに、タスクによって、以前行ったカスタマイズを上書きせずに、スキーマの修正を考慮するためにフォームを再生成することができます。
CRUDジェネレータ
これでフォームクラスが生成されたので、ブラウザからオブジェクトを扱うためにsymfonyモジュールを作る作業がどんなに簡単なことであるか見てみましょう。Article
、Author
、Category
、とTag
クラスからオブジェクトの作成、修正、、削除を行うことを考えてみます。
Author
クラス用のモジュールを作ることを始めましょう。手作業でモジュールを作るとしても、Doctrineのプラグインはオブジェクトモデルに基づいてCRUDモジュールを生成するdoctrine:generate-crud
タスクを提供します。以前のセクションで生成したフォームを使います:
$ ./symfony doctrine:generate-crud frontend author Author
doctrine:generate-crud
タスクは3つの引数を受け取ります:
frontend
: モジュールを作りたいアプリケーションの名前author
: 作りたいモジュールの名前Author
: モジュールのために作りたいモデルクラスの名前
note
CRUDはCreation / Retrieval / Update / Deletionを表しモデルのデータで実行できる基本的な4つのオペレーションを要約しています。
リスト4-4において、Author
クラスのオブジェクトを一覧表示(index
)、修正(edit
)、保存(update
)、と削除(delete
)できるようにする5つのアクションをタスクが生成したことを見ます。
リスト4-4 - タスクによって生成されたauthorActions
クラス
// apps/frontend/modules/author/actions/actions.class.php class authorActions extends sfActions { public function executeIndex() { $this->authorList = $this->getAuthorTable()->findAll(); } public function executeCreate() { $this->form = new AuthorForm(); $this->setTemplate('edit'); } public function executeEdit($request) { $this->form = $this->getAuthorForm($request->getParameter('id')); } public function executeUpdate($request) { $this->forward404Unless($request->isMethod('post')); $this->form = $this->getAuthorForm($request->getParameter('id')); $this->form->bind($request->getParameter('author')); if ($this->form->isValid()) { $author = $this->form->save(); $this->redirect('author/edit?id='.$author->get('id')); } $this->setTemplate('edit'); } public function executeDelete($request) { $this->forward404Unless($author = $this->getAuthorById($request->getParameter('id'))); $author->delete(); $this->redirect('author/index'); } private function getAuthorTable() { return Doctrine::getTable('Author'); } private function getAuthorById($id) { return $this->getAuthorTable()->find($id); } private function getAuthorForm($id) { $author = $this->getAuthorById($id); if ($author instanceof Author) { return new AuthorForm($author); } else { return new AuthorForm(); } } }
このモジュールにおいて、フォームのライフサイクルは3つのメソッド: create
、edit
と、update
メソッドによって対処されます。--non-atomic-actions
オプションを付けることで、doctrine:generate-crud
タスクに以前の3つのメソッドの機能のみをカバーする1つのメソッドだけを生成するように頼むこともできます:
$ ./symfony doctrine:generate-crud frontend author Author --non-atomic-actions
--non-atomic-actions
(リスト4-5)を利用して生成されたコードはより簡潔でより冗長ではありません。
リスト4-5 - --non-atomic-actions
オプションで生成されたauthorActions
クラス
class authorActions extends sfActions { public function executeIndex() { $this->authorList = $this->getAuthorTable()->findAll(); } public function executeEdit($request) { $this->form = new AuthorForm(Doctrine::getTable('Author')->find($request->getParameter('id'))); if ($request->isMethod('post')) { $this->form->bind($request->getParameter('author')); if ($this->form->isValid()) { $author = $this->form->save(); $this->redirect('author/edit?id='.$author->getId()); } } } public function executeDelete($request) { $this->forward404Unless($author = Doctrine::getTable('Author')->find($request->getParameter('id'))); $author->delete(); $this->redirect('author/index'); } }
タスクはindexSuccess
とeditSuccess
の2つのテンプレートも生成しました。<?php echo $form ?>
ステートメントを使わずにeditSuccess
テンプレートが生成されました。--non-verbose-templates
を使用して、このふるまいを修正できます:
$ ./symfony doctrine:generate-crud frontend author Author --non-verbose-templates
リスト4-6が示すように、このオプションはプロトタイプの期間で便利です。
リスト4-6 - editSuccess
テンプレート
// apps/frontend/modules/author/templates/editSuccess.php <?php $author = $form->getObject() ?> <h1><?php echo $author->isNew() ? 'New' : 'Edit' ?> Author</h1> <form action="<?php echo url_for('author/edit'.(!$author->isNew() ? '?id='.$author->getId() : '')) ?>" method="post" <?php $form->isMultipart() and print 'enctype="multipart/form-data" ' ?>> <table> <tfoot> <tr> <td colspan="2"> <a href="<?php echo url_for('author/index') ?>">Cancel</a> <?php if (!$author->isNew()): ?> <?php echo link_to('Delete', 'author/delete?id='.$author->getId(), array('post' => true, 'confirm' => 'Are you sure?')) ?> <?php endif; ?> <input type="submit" value="Save" /> </td> </tr> </tfoot> <tbody> <?php echo $form ?> </tbody> </table> </form>
tip
--with-show
オプションによってオブジェクトを閲覧する(読み込みだけ)ために使うアクションとテンプレートを生成できます。
生成モジュール(図4-1と図4-2)を見るために/frontend_dev.php/author
のURLをブラウザで開くことができます。インターフェイスで少し遊んでみてください。生成モジュールのおかげで著者の一覧を見る、新しい筆者を追加、修正、削除することさえできます。バリデーションのルールも機能していることもわかります。
図4-1 - 著者の一覧
図4-2 - バリデーションエラーがある著者を編集する
Article
クラスでこのオペレーションを繰り返すことができます:
$ ./symfony doctrine:generate-crud frontend article Article --non-verbose-templates --non-atomic-actions
生成コードはAuthor
クラスのコードとよく似ています。しかしながら、新しい記事を作ろうとすると、コードは図4-3で見られるような致命的エラーが投げられます。
図4-3 - リンクされたテーブルは__toString()
メソッドを定義しなければならない
ArticleForm
フォームはArticle
オブジェクトとAuthor
オブジェクトの間のリレーションを表現するためにsfWidgetFormDoctrineSelect
ウィジェットを使います。このウィジェットは著者のドロップダウンリストを作ります。表示の間、__toString()
マジックメソッドを使用してAuthorオブジェクトは文字列に変換されます。リスト4-7で示されるように__toString()
マジックメソッドはAuthor
クラスの中で定義しなけばなりません。
リスト4-7 - Author
クラスのために__toString()
メソッドを実装する
class Author extends BaseAuthor { public function __toString() { return $this->getFirstName().' '.$this->getLastName(); } }
Author
クラスのように、他のモデルのクラス: Article
、Category
、とTag
用の__toString()
メソッドを作ることができます
note
sfDoctrineRecordは基底の__toString()が指定されていない場合それを推測しようとします。文字列の表現として使うために、これはタイトル、名前、題目、などの名前を持つカラムをチェックします。
tip
sfWidgetFormDoctrineSelect
ウィジェットのmethod
オプションはオブジェクトをテキスト形式で表現するために使われるメソッドを変更します。
図4-4は__toString()
メソッドを実装した後で記事を作る方法を示します。
図4-4 - 記事を作る
生成フォームをカスタマイズする
doctrine:build-forms
とdoctrine:generate-crud
タスクによってモデルオブジェクトの一覧を表示、作成、編集する機能を持つモジュールを作ることができます。これらのモジュールはモデルのバリデーションルールだけでなくテーブル間のリレーションも考慮されます。これらすべては一行のコードを書かずに行われます!
生成コードをカスタマイズしましょう。フォームクラスがすでに多くの要素を考慮しているのであれば、いくつかの面でカスタマイズする必要があります。
バリデータとウィジェットを設定する
デフォルトで生成されたバリデータとウィジェットを設定することを始めましょう。
ArticleForm
フォームはslug
フィールドを持ちます。スラッグはURLの中で記事を一意的に表す文字列です。例えば、タイトルが"Optimize the developments with symfony"である記事のslugは12-optimize-the-developments-with-symfony
で、12
は記事のid
です。オブジェクトが保存されたときこのフィールドはtitle
に従って自動的に算出されますが、これは明らかにユーザーによって上書きされる可能性があります。このフィールドがスキーマに必要であるとしても、フォームに必須なものになることはありません。リスト4-8で示されるように、これがバリデータを修正してオプションを追加する理由です。content
フィールドのサイズを増やしてユーザーが少なくとも5文字を入力するようにもカスタマイズします。
リスト4-8 - バリデータとウィジェットをカスタマイズする
class ArticleForm extends BaseArticleForm { public function configure() { // ... $this->validatorSchema['slug']->setOption('required', false); $this->validatorSchema['content']->setOption('min_length', 5); $this->widgetSchema['content']->setAttributes(array('rows' => 10, 'cols' => 40)); } }
ここではvalidatorSchema
とwidgetSchema
オブジェクトをPHP配列として使います。これらの配列はフィールドの名前をキーとして取り、バリデータオブジェクトとウィジェットの関連オブジェクトをそれぞれ返します。フィールドとウィジェットを個別にカスタマイズできます。
note
オブジェクトをPHP配列として使うために、sfValidatorSchema
とsfWidgetFormSchema
クラスがPHP5以降で利用可能なArrayAccess
インターフェイスを実装します。
2つの記事が同じslug
を持たないことを確認するために、一意性の制約がスキーマの定義に追加されました。データベースレベルでのこの制約はsfValidatorDoctrineUnique
バリデータを使用してArticleForm
フォームに反映されます。このバリデータは任意のフォームフィールドの一意性をチェックできます。これはとりわけログインのEメールアドレスの一意性をチェックするために役立ちます。リスト4-9はArticleForm
フォームの中でこれを使う方法を示しています。
リスト4-9 - フィールドの一意性をチェックするためにsfValidatorDoctrineUnique
バリデータを使う
class BaseArticleForm extends BaseFormDoctrine { public function setup() { // ... $this->validatorSchema->setPostValidator( new sfValidatorDoctrineUnique(array('model' => 'Article', 'column' => array('slug'))) ); } }
sfValidatorDoctrineUnique
バリデータはそれぞれのフィールドの個別のバリデーションを行った後にデータ全体に実行するpostValidator
です。slug
の一意性をバリデートするには、バリデータはslug
の値だけでなく主キーにもアクセスできなければなりません。slugは記事の更新の間は同じ状態を保つことができるので記事の作成と編集のすべての間においてバリデーションのルールは本当に異なります。
著者がアクティブであるか知るために、author
テーブルのactive
フィールドをカスタマイズしてみましょう。リスト4-10は、author_id
フィールドに接続されるsfWidgetFormDoctrineSelect
ウィジェットのquery
オプションを修正することで、ArticleForm
フォームからアクティブではない著者を排除する方法を示しています。query
オプションはDoctrine_Queryオブジェクトを受け取ります。これによってローリングリストの利用可能なオプションの一覧を絞ることができきます。
リスト4-10 - sfWidgetFormDoctrineSelect
ウィジェットをカスタマイズする
class ArticleForm extends BaseArticleForm { public function configure() { // ... $query = Doctrine_Query::create() ->from('Author a') ->where('a.active = ?', true); $this->widgetSchema['author_id']->setOption('query', $query); } }
ウィジェットのカスタマイズによって利用可能なオプションのリストの範囲を絞ることができるとしても、リスト4-11で示されるように、バリデータレベル上で絞るこれを忘れてはなりません。sfWidgetProperSelect
ウィジェットのように、sfValidatorDoctrineChoice
バリデータはフィールドに対して有効なオプションの範囲を絞るためにquery
オプションを受け取ります。
リスト4-11 - sfValidatorDoctrineChoice
バリデータをカスタマイズする
class ArticleForm extends BaseArticleForm { public function configure() { // ... $query = Doctrine_Query::create() ->from('Author a') ->where('a.active = ?', true); $this->widgetSchema['author_id']->setOption('query', $query); $this->validatorSchema['author_id']->setOption('query', $query); } }
前の例ではconfigure()
メソッドの中でQuery
オブジェクトを直接定義しました。我々のプロジェクトにおいて、このクエリは他の状況においても必ず役に立ちますので、リスト4-12が示すようにAuthorPeer
クラスの範囲内でgetActiveAuthorsQuery()
メソッドを作りArticleForm
からこのメソッドを呼び出す方がベターです。
リスト4-12 - モデルのQuery
をリファクタリングする
class AuthorTable extends Doctrine_Table { public function getActiveAuthorsQuery() { $query = Doctrine_Query::create() ->from('Author a') ->where('a.active = ?', true); return $query; } } class ArticleForm extends BaseArticleForm { public function configure() { $authorQuery = Doctrine::getTable('Author')->getActiveAuthorsQuery(); $this->widgetSchema['author_id']->setOption('query', $authorQuery); $this->validatorSchema['author_id']->setOption('query', $authorQuery); } }
tip
sfWidgetFormDoctrineSelect
ウィジェットとsfValidatorDoctrineChoice
バリデータが2つのテーブル間の一対多のリレーションを表すように、sfWidgetDoctrineSelectMany
とsfValidatorDoctrineChoiceMany
バリデータは多対多のリレーションを表し同じオプションを受け取ります。ArticleForm
フォームにおいて、これらのクラスはartcile
テーブルとtag
テーブルの間のリレーションを表すために使われます。
バリデータを変更する
email
はスキーマの中でstring(255)
として定義されたので、symfonyは文字数の最大長を255字に制限するsfValidatorString()
バリデータを作りました。このフィールドは有効なEメールを受け取ることにもなっており、リスト4-14は生成されたバリデータをsfValidatorEmail
バリデータに置き換えます。
リスト4-13 - AuthorForm
クラスのemail
フィールドのバリデータを変更する
class AuthorForm extends BaseAuthorForm { public function configure() { $this->validatorSchema['email'] = new sfValidatorEmail(); } }
バリデータを追加する
前の章で生成されたバリデータを修正する方法を観察しました。email
フィールドの場合、これは最大長のバリデーションを維持するために便利です。リスト4-14において、Eメールの妥当性を保証してフィールドに対して認められた最大の長さをチェックするためにsfValidatorAnd
バリデータを使います。
リスト4-14 - 複数のバリデータを使う
class AuthorForm extends BaseAuthorForm { public function configure() { $this->validatorSchema['email'] = new sfValidatorAnd(array( new sfValidatorString(array('max_length' => 255)), new sfValidatorEmail(), )); } }
以前の例は完璧ではありません。データベースのスキーマ内のemail
フィールドの長さを後で修正することを決める場合、フォームの中でもこれを考えなければならなくなるからです。生成されたバリデータを置き換える代わりに、リスト4-15で示されるように、バリデータを追加する方がベターです。
リスト4-15 - バリデータを追加する
class AuthorForm extends BaseAuthorForm { public function configure() { $this->validatorSchema['email'] = new sfValidatorAnd(array( $this->validatorSchema['email'], new sfValidatorEmail(), )); } }
ウィジェットを変更する
データベースのスキーマにおいて、article
テーブルのstatus
フィールドは記事のステータスを文字列として保存します。リスト4-16で示されるように、利用可能な値はArticePeer
クラスの中で定義されました。
リスト4-16 - 利用可能なステータスをArticlePeer
クラスの中で定義する
class ArticleTable extends Doctrine_Table { static protected $statuses = array('draft', 'online', 'offline'); static public function getStatuses() { return self::$statuses; } // ... }
記事を編集するとき、status
フィールドはテキストフィールドの代わりにドロップダウンのリストとして表されなければなりません。これを行うには、リスト4-17で示されるように、先ほど使用したウィジェットを変更してみましょう。
リスト4-17 - status
フィールドのためのウィジェットを変更する
class ArticleForm extends BaseArticleForm { public function configure() { $this->widgetSchema['status'] = new sfWidgetFormSelect(array('choices' => ArticleTable::getStatuses())); } }
完全にするために、選択されたステータスが実際に可能なオプションのリストに所属することを確かめるためにバリデータも変更しなければなりません(リスト4-18)。
リスト4-18 - status
フィールドバリデータを修正する
class ArticleForm extends BaseArticleForm { public function configure() { $statuses = ArticleTable::getStatuses(); $this->widgetSchema['status'] = new sfWidgetFormSelect(array('choices' => $statuses)); $this->validatorSchema['status'] = new sfValidatorChoice(array('choices' => array_keys($statuses))); } }
フィールドを削除する
article
テーブルはcreated_at
とupdated_at
の特別なカラムを2つ持ちます。それぞれのカラムはDoctrineによって自動的に扱われます。リスト4-19が示すように、ユーザーがこれらのカラムを修正しないように、これらのカラムをフォームから削除しなければなりません。
リスト4-19 - フィールドを削除する
class ArticleForm extends BaseArticleForm { public function configure() { // ... unset($this->validatorSchema['created_at']); unset($this->widgetSchema['created_at']); unset($this->validatorSchema['updated_at']); unset($this->widgetSchema['updated_at']); unset($this->validatorSchema['published_at']); unset($this->widgetSchema['published_at']); } }
フィールドを削除するには、フィールドのバリデータとウィジェットを削除することが必要です。フォームをPHP配列として使うことで、両方を一度に削除することも可能であることを示しています。
リスト4-20 - フォームをPHP配列として使用してフィールドを削除する
class ArticleForm extends BaseArticleForm { public function configure() { unset($this['created_at'], $this['updated_at'], $this['published_at']); } }
要約
要約するために、リスト4-21とリスト4-22はカスタマイズするArticleForm
とAuthorForm
フォームを示します。
リスト4-21 - ArticleForm
フォーム
class ArticleForm extends BaseArticleForm { public function configure() { $authorQuery = Doctrine::getTable('Author')->getActiveAuthorsQuery(); // ウィジェット $this->widgetSchema['content']->setAttributes(array('rows' => 10, 'cols' => 40)); $this->widgetSchema['status'] = new sfWidgetFormSelect(array('choices' => ArticleTable::getStatuses())); $this->widgetSchema['author_id']->setOption('query', $authorQuery); // バリデータ $this->validatorSchema['slug']->setOption('required', false); $this->validatorSchema['content']->setOption('min_length', 5); $this->validatorSchema['status'] = new sfValidatorChoice(array('choices' => array_keys(ArticleTable::getStatuses()))); $this->validatorSchema['author_id']->setOption('query', $authorQuery); unset($this['created_at'], $this['updated_at'], $this['published_at']); } }
リスト4-22 - AuthorForm
フォーム
class AuthorForm extends BaseAuthorForm { public function configure() { $this->validatorSchema['email'] = new sfValidatorAnd(array( $this->validatorSchema['email'], new sfValidatorEmail(), )); } }
doctrine:build-forms
を使うことでフォームはオブジェクトモデルをイントロスペクトしてたいていの要素を自動的に生成できます。この自動化はいくつかの理由から役に立ちます:
これによって繰り返される冗長な作業をしなくても済むので、開発者の人生が楽になります。開発者はプロジェクト固有のビジネスルールに従ってバリデータとウィジェットのカスタマイズに集中できます。
加えて、データベースのスキーマが更新されたとき、生成されたフォームは自動的に更新されます。開発者は自らが行ったカスタマイズを調整しなければなりません。
次のセクションではアクションのカスタマイズとdoctrine:generate-crud
タスクによって生成されたテンプレートを説明します。
フォームのシリアライズ
以前の章ではdoctrine:build-forms
タスクによって生成されたフォームをカスタマイズする方法を示しました。現在のセクションにおいて、doctrine:generate-crud
タスクで生成されたコードから始めて、フォームのライフサイクルをカスタマイズすることにします。
デフォルトの値
Doctrineフォームのインスタンスは常にDoctrineオブジェクトに接続されます。リンクされたDoctrineオブジェクトはgetModelName()
メソッドによって返されたクラスに常に所属します。例えば、AuthorForm
フォームはAuthor
クラスに所属するオブジェクトのみにリンクできます。このオブジェクトは空のオブジェクト(Author
クラスの空のインスタンス)、もしくはコンストラクタに最初の引数として送り出されるオブジェクトのどちらかです。"平均的な"フォームのコンストラクタは値の配列を最初の引数として受け取るのに対して、DoctrineフォームのコンストラクタはDoctrineのオブジェクトを受け取ります。このオブジェクトはそれぞれのフィールドのデフォルトの値を定義するために使われます。getObject()
メソッドは現在のインスタンスに関連するオブジェクトを返しisNew()
メソッドによってオブジェクトがコンストラクタを通して送り出されたことを知ることができます:
// 新しいオブジェクトを作る $authorForm = new AuthorForm(); print $authorForm->getObject()->getId(); // nullを出力する print $authorForm->isNew(); // trueを出力する // 既存のオブジェクトを修正する $author = Doctrine::getTable('Author')->find(1); $authorForm = new AuthorForm($author); print $authorForm->getObject()->getId(); // 1を出力する print $authorForm->isNew(); // falseを出力する
ライフサイクルに対処する
この章の始めで見たように、リスト4-23で示される、edit
アクションは、フォームのライフサイクルに対処します。
リスト4-23 - author
モジュールのexecuteEdit
メソッド
// apps/frontend/modules/author/actions/actions.class.php class authorActions extends sfActions { // ... public function executeEdit($request) { $author = Doctrine::getTable('Author')->find($request->getParameter('id')); $this->form = new AuthorForm($author); if ($request->isMethod('post')) { $this->form->bind($request->getParameter('author')); if ($this->form->isValid()) { $author = $this->form->save(); $this->redirect('author/edit?id='.$author->getId()); } } } }
edit
アクションが以前の章で説明されたアクションのように見えるとしても、わずかな違いを指摘できます:
Author
クラスからのDoctrineオブジェクトはフォームコンストラクタに最初の引数として送り出されます:$author = Doctrine::getTable('Author')->find($request->getParameter('id')); $this->form = new AuthorForm($author);
関連テーブル(
author
)から名付けられたPHP配列形式で入力データを取得できるようにウィジェットのname
属性のフォーマットは自動的にカスタマイズされます:$this->form->bind($request->getParameter('author'));
フォームが有効なとき、
save()
メソッドを呼び出すだけでフォームに関連したDoctrineのオブジェクトが作成もしくは更新されます:$author = $this->form->save();
Doctrineオブジェクトを作り修正する
リスト4-23のコードはAuthor
クラスからのオブジェクトの単独のメソッドの作成と修正を扱います:
新しい
Author
オブジェクトを作る:index
アクションはid
パラメータ無しで呼び出される($request->getParameter('id')
がnull
)それゆえ
find()
の呼び出しはnull
を送り出すform
オブジェクトはDoctrineの空のAuthor
オブジェクトにリンクされる有効なフォームが投稿されたときに
$this->form->save()
の呼び出しは結果として新しいAuthor
オブジェクトを作る
既存の
Author
オブジェクトの修正:index
アクションはid
パラメータで呼び出される($request->getParameter('id')
はAuthor
オブジェクトが修正する主キーを表す)find()
メソッドの呼び出しは主キーに関連するAuthor
オブジェクトを返すform
オブジェクトはそれゆえ以前に見つかったオブジェクトにリンクされる有効なフォームが投稿されたとき
$this->form->save()
呼び出しはAuthor
オブジェクトを更新する
save()メソッド
Doctrineのフォームが有効なとき、save()
メソッドは関連オブジェクトを更新してそれをデータベースに保存します。このメソッドはメインのオブジェクトだけでなく潜在的に関連するオブジェクトも実際に保存します。例えば、ArticleForm
フォームは記事に関連するタグを更新します。article
テーブルとtag
テーブルのリレーションは多対多なので、(生成されたsaveArticleTagList()
メソッドを利用して)記事に関連するタグはarticle_tag
テーブルに保存されます。
一貫したシリアライゼーションを保証するために、save()
メソッドは1つのトランザクションの中のすべての更新を含みます。
note
9章でsave()
メソッドが国際化テーブルも自動的に更新することを見ることになります。
ファイルのアップロードを扱う
save()
メソッドはDoctrineのオブジェクトを自動的に更新しますがファイルアップロードを管理するその他の要素を扱うことはできません。
ファイルをそれぞれの記事に添付する方法を見てみましょう。リスト4-24で示されるように、ファイルはweb/uploads
ディレクトリに保存されファイルパスへの参照はarticle
テーブルのfile
フィールドに保存されます。
リスト4-24 - 関連するファイルを持つarticle
テーブル用のスキーマ
// config/schema.yml doctrine: article: // ... file: string(255)
すべてのスキーマを更新した後で、オブジェクトモデル、データベースと関連フォームを更新する必要があります:
$ ./symfony doctrine:build-all
caution
doctrine:build-all
タスクはすべてのスキーマテーブルを再生成するためにこれらを削除することに気を付けてください。テーブル内のデータはそれゆえ上書きされます。これがそれぞれのモデルを修正するたびに再びダウンロードできるテストデータ(fixtures
)を作ることが重要である理由です。
リスト4-25はウィジェットとバリデータをfile
フィールドにリンクするためにArticleForm
クラスを修正する方法を示しています。
リスト4-25 - ArticleForm
フォームのfile
フィールドを修正する
class ArticleForm extends BaseArticleForm { public function configure() { // ... $this->widgetSchema['file'] = new sfWidgetFormInputFile(); $this->validatorSchema['file'] = new sfValidatorFile(); } }
ファイルのアップロードを許可するフォームに関しては、enctype
属性をテンプレートのform
タグに追加することも忘れないでください(ファイルのアップロード管理に関する詳細な情報は2章を参照)。
リスト4-26はファイルをサーバー上にアップロードしてそのパスをarticle
オブジェクトに保存するときに適用する修正方法を示しています。
リスト4-26 - article
オブジェクトとアクションの中でアップロードされたファイルを保存する
public function executeEdit($request) { $author = Doctrine::getTable('Author')->find($request->getParameter('id')); $this->form = new ArticleForm($author); if ($request->isMethod('post')) { $this->form->bind($request->getParameter('article'), $request->getFiles('article')); if ($this->form->isValid()) { $file = $this->form->getValue('file'); $filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension()); $file->save(sfConfig::get('sf_upload_dir').'/'.$filename); $article = $this->form->save(); $this->redirect('article/edit?id='.$article->getId()); } } }
アップロードされたファイルをファイルシステム上に保存すればsfValidatedFile
オブジェクトはファイルへの絶対パスを知ることができます。save()
メソッドを呼び出している間に、関連オブジェクトを更新するためにフィールドの値が使われ、file
フィールドに関しては、ファイルへの絶対パスを送り返しながら、__toString()
メソッドのおかげでsfValidatedFile
オブジェクトは文字列に変換されます。article
テーブルのfile
カラムはこの絶対パスを保存します。
tip
sfConfig::get('sf_upload_dir')
ディレクトリへの相対パスを保存したいのであれば、sfValidatedFile
を継承するクラスを作りsfValidatorFile
バリデータに新しいクラスの名前を送り出すためにvalidated_file_class
オプションを使うことができます。バリデータはクラスのインスタンスを返します。この章の残りで別のアプローチを見ることになります。こちらのアプローチはオブジェクトをデータベースに保存する前にfile
カラムの値の修正作業で構成されます。
save()メソッドをカスタマイズする
以前のセクションでedit
アクションの中でアップロードされたファイルを保存する方法を見ました。オブジェクト指向プログラミングの原則の1つはクラスのカプセル化によるコードの再利用です。ファイルを保存するためにArticleForm
フォームを利用してそれぞれのアクションごとにコードを重複させる代わりに、コードをArticleForm
クラスに移動させる方がベターです。リスト4-27はファイルの保存と場合によって既存のファイルを削除するためにsave()
メソッドをオーバーライドする方法を示しています。
リスト4-27 - ArticleForm
クラスのsave()
メソッドをオーバーライドする
class ArticleForm extends BaseFormDoctrine { // ... public function save($con = null) { if (file_exists($this->getObject()->getFile())) { unlink($this->getObject()->getFile()); } $file = $this->getValue('file'); $filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension()); $file->save(sfConfig::get('sf_upload_dir').'/'.$filename); return parent::save($con); } }
コードをフォームに移動させた後で、edit
アクションはdoctrine:generate-crud
タスクによって初期に生成されたコードとまったく同じです。
doSave()メソッドをカスタマイズする
保存に関連するそれぞれのオペレーションが正しく処理されることを保証するためにオブジェクトの保存はトランザクションの範囲内で行われたことを見ました。アップロードされたファイルを保存するために我々が以前のセクションで行ったようにsave()
メソッドをオーバーライドするとき、実行されたコードはこのトランザクションから独立しています。
リスト4-28はアップロードされたファイルを保存するグローバルなトランザクションのコードに挿入するdoSave()
メソッドを方法を示しています。
リスト4-28 - ArticleForm
フォームの中のdoSave()
メソッドをオーバーライドする
class ArticleForm extends BaseFormDoctrine { // ... protected function doSave($con = null) { if (file_exists($this->getObject()->getFile())) { unlink($this->getObject()->getFile()); } $file = $this->getValue('file'); $filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension()); $file->save(sfConfig::get('sf_upload_dir').'/'.$filename); return parent::doSave($con); } }
doSave()
メソッドはsave()
メソッドによって作られたトランザクションの中で呼び出され、file()
オブジェクトのsave()
メソッドへの呼び出しが例外を投げる場合、オブジェクトは保存されません。
updateObject()メソッドをカスタマイズする
データベースの更新と保存の間にフォームに接続されるオブジェクトを修正することが時々必要です。
我々のファイルのアップロードの例において、アップロードされたファイルへの絶対パスをfile
カラムに保存する代わりに、sfConfig::get('sf_upload_dir')
ディレクトリへの相対パスを保存したい場合を考えます。
リスト4-29はオブジェクトが自動的に更新された後に保存される前にfile
カラムの値を変更するためにArticleForm
フォームのupdateObject()
メソッドをオーバーライドする方法を示しています。
リスト4-29 - updateObject()
メソッドとArticleForm
クラスをオーバーライドする
class ArticleForm extends BaseFormDoctrine { // ... public function updateObject($values = null) { $object = parent::updateObject($values); $object->setFile(str_replace(sfConfig::get('sf_upload_dir').'/', '', $object->getFile())); return $object; } }
updateObject()
メソッドはオブジェクトをデータベースに保存する前にdoSave()
メソッドによって呼び出されます。
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.