Web プロジェクトにおいて、たいていのフォームはモデルオブジェクトを作成もしくは修正するために使われます。これらのオブジェクトは ORM のおかげで通常はシリアライズされデータベースに保存されます。symfony のフォームシステムは ORM の Doctrine とやりとりするための追加レイヤーを提供します。これによってこれらのモデルオブジェクトにもとづくフォームの実装がかんたんになります。
この章ではフォームと Doctrine のオブジェクトモデルを統合する方法をくわしく説明します。Doctrine と symfony の統合機能に慣れていることが大いに推奨されます。 そうでなければ、「symfony and Doctrine book:(/doctrine/1_2/ja/)をご参照ください。
はじめる前に
この章において、記事の管理システムを作ります。データベースのスキーマからはじめてみましょう。これはリスト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 sfWidgetFormInputText(), 'last_name' => new sfWidgetFormInputText(), 'email' => new sfWidgetFormInputText(), )); $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(sfWebRequest $request) { $this->form = $this->getAuthorForm($request->getParameter('id')); } public function executeUpdate(sfWebRequest $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(sfWebRequest $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(sfWebRequest $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(sfWebRequest $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
オプションをつけることでオブジェクトを閲覧する (読み込みだけ) ために使うアクションとテンプレートを生成できます。
URL の /frontend_dev.php/author
をブラウザで開いて、生成モジュールを見ることができます (図4-1と図4-2)。インターフェイスで少し遊んでみてください。
生成モジュールのおかげで著者の一覧を見る、新しい筆者を追加、修正、削除することさえできます。バリデーションのルールも機能していることもわかります。
図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」である記事のスラッグは 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
クラスは ArrayAccess
インターフェイスを実装しています。
2つの記事が同じスラッグをもっていないことを確認するために、一意性の制約がスキーマの定義に追加されました。データベースレベルでのこの制約は sfValidatorDoctrineUnique
バリデータを使って ArticleForm
フォームに反映されます。このバリデータは任意のフォームフィールドの一意性をチェックできます。これはとりわけログインのメールアドレスの一意性をチェックするために役立ちます。リスト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
バリデータはそれぞれのフィールドの個別のバリデーションを行った後にデータ全体に実行するポストバリデータです。スラッグ の一意性のバリデーションを行うには、バリデータはスラッグの値だけでなくプライマリキーにもアクセスできなければなりません。スラッグは記事の更新のあいだは同じ状態を保つことができるので記事の作成と編集のすべてのあいだにおいてバリデーションのルールは本当に異なります。
著者がアクティブであるか知るために使われる author
テーブルの active
フィールドをカスタマイズしてみましょう。リスト4-10は author_id
に接続される FormDoctrineSelect
ウィジェットの 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 - モデルのクエリをリファクタリングする
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); } }
バリデータを変更する
email
はスキーマのなかで string(255)
として定義されたので、symfony は文字数の最大長を255字に制限する sfValidatorString()
バリデータを作りました。このフィールドは妥当なメールアドレスを受け入れられるようになっており、リスト4-14は生成されたバリデータを sfValidatorEmail
バリデータに置き換えます。
リスト4-13 - AuthorForm
クラスの email
フィールドのバリデータを変更する
class AuthorForm extends BaseAuthorForm { public function configure() { $this->validatorSchema['email'] = new sfValidatorEmail(); } }
バリデータを追加する
以前の章で生成されたバリデータを修正する方法を観察しました。email
フィールドの場合、これは最大長のバリデーションを維持するために便利です。リスト4-14において、メールアドレスの妥当性を保証してフィールドに対して許容される最大の長さをチェックするために 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
が用意されています。それぞれのカラムは 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
クラスの空のインスタンス)、もしくはコンストラクタに第1引数として送り出されるオブジェクトのどちらかです。「平均的な」フォームのコンストラクタは値の配列を第1引数として受け取るのに対して、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(sfWebRequest $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 オブジェクトはフォームコンストラクタに第1引数として送り出されます:$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
タスクはすべてのスキーマテーブルを再生成するためにこれらを削除することにご注意ください。テーブル内のデータはそれゆえオーバーライドされます。これがそれぞれのモデルを修正するたびに再びダウンロードできるテストデータ (フィクスチャ) を作ることが重要である理由です。
リスト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(sfWebRequest $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.