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

第11章 - Doctrineとの統合

1.1
Symfony version Language

Webプロジェクトにおいて、たいていのフォームはモデルオブジェクトを作るもしくは修正するために使われます。通常これらのオブジェクトはORMのおかげでシリアライズ化されます。symfonyのフォームシステムはsymfonyに組み込まれたORMであるDoctrineと橋渡しをするための追加レイヤーを提供し、これらのモデルオブジェクトに基づくフォームの実装を簡単にします。

この章ではフォームをDoctrineのオブジェクトモデルに統合する方法を詳しく説明します。Doctrineをsymfonyに統合して使うことにすでに慣れているのであれば大いにお勧めします。そうでなければ、"The Definitive Guide to symfony"のモデルレイヤーの内側の章を参照してください。

始める前に

この章において、記事の管理システムを作ります。データベースのスキーマから始めてみましょう。これはリスト4-1で示されるような5つのテーブル:articleauthorcategorytag、と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

テーブル間のリレーションは次の通りです:

  • articleauthorテーブルは一対多のリレーション: 記事は1人の著者によってのみ書かれる
  • articlecategoryテーブルは一対多のリレーション: 記事は1つのカテゴリに所属するもしくはまったく所属しない
  • articletagテーブル間の多対多のリレーション

フォームクラスを生成する

articleauthorcategory、とtagテーブルの情報を編集することを考えます。これを行うには、これらのそれぞれのテーブルにリンクされたフォームを作りデータベースのスキーマに関連するウィジェットとスキーマを設定する必要があります。これらのフォームをマニュアル通りに作ることは可能ですが、長く退屈なタスクで、結局の所、複数のファイルにおいて同じ種類の情報の反復が強制させられます(カラムとフィールド名、カラムとフィールドの最大サイズ・・・)。さらに、モデルを変更するたびに、関連するフォームクラスも変更しなければなりません。幸いにして、Doctrineプラグインに組み込まれているdoctrine:build-formsタスクはオブジェクトモデルに関連するフォームを生成するこの作業を自動化します:

$ ./symfony doctrine:build-forms

フォーム生成の間に、タスクはモデルのイントロスペクションを利用しテーブル間のリレーションを考慮してテーブルごとにそれぞれのカラム用のバリデータとウィジェットを持つ1つのクラスを作ります。

note

doctrine:build-alldoctrine:build-all-loaddoctrine: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テーブルはBaseAuthorFormAuthorFormクラスで構成され、これらのクラスはlib/form/base/BaseAuthorForm.class.phplib/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クラスを返す

sidebar

Doctrineフォームのグローバルなカスタマイズ

それぞれのテーブル用に生成されたクラスに加えて、doctrine:build-formsBaseFormDoctrineクラスも生成します。この空のクラスはlib/form/base/ディレクトリの中の他のすべての生成クラスの基底クラスであり、Doctrineのすべてのフォームのふるまいをグローバルに設定することを可能にします。例えば、すべてのDoctrineフォーム用のデフォルトのフォーマッターを簡単に変更できます:

abstract class BaseFormDoctrine extends sfFormDoctrine
{
  public function setup()
  {
    sfWidgetFormSchema::setDefaultFormFormatterName('div');
  }
}

BaseFormDoctrineクラスがsfFormDoctrineクラスを継承することがわかります。このクラスはDoctrine固有の機能に連動せずデータベースにおいてフォームに投稿された値からのオブジェクトのシリアライズを処理します。

tip

基底クラスは設定のためにconfigure(メソッドの代わりにsetup()メソッドを使います。これによって開発者はparent::configure()`の呼び出しに対処せずに空の生成クラスの設定を上書きできます。

フォームのフィールド名はスキーマ: idfirst_namelast_name、とemailに設定するカラム名に対して理想的なものです。

authorテーブルのそれぞれのカラムに対して、doctrine:build-formsタスクはスキーマの定義に従ってウィジェットとバリデータを生成します。タスクは可能な限り常に最も安全なバリデータを生成します。idフィールドを考えてみましょう。値が有効な整数であるかチェックできます。代わりにここで生成されたバリデータによって(既存のオブジェクトを編集するために)識別子が実際に存在するもしくは(新しいオブジェクトを作ることができるように)識別子が空であることをバリデートできます。これはより強いバリデーションです。

生成フォームは即座に使うことができます。<?php echo $form ?>ステートメントを追加すれば、コードを一行も書かずにバリデーション機能を持つフォームを作ることができます。

プロトタイプを素早く作る機能を越えて、生成クラスを修正しなくても生成フォームを拡張するのは簡単です。これは基底とフォームクラスの継承システムのおかげです。

最後にデータベースのスキーマを拡張するたびに、タスクによって、以前行ったカスタマイズを上書きせずに、スキーマの修正を考慮するためにフォームを再生成することができます。

CRUDジェネレータ

これでフォームクラスが生成されたので、ブラウザからオブジェクトを扱うためにsymfonyモジュールを作る作業がどんなに簡単なことであるか見てみましょう。ArticleAuthorCategory、と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つのメソッド: createeditと、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');
  }
}

タスクはindexSuccesseditSuccessの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">
          &nbsp;<a href="<?php echo url_for('author/index') ?>">Cancel</a>
          <?php if (!$author->isNew()): ?>
            &nbsp;<?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()メソッドを定義しなければならない

リンクされたテーブルは<code>__toString()</code>メソッドを定義しなければならない

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クラスのように、他のモデルのクラス: ArticleCategory、とTag用の__toString()メソッドを作ることができます

note

sfDoctrineRecordは基底の__toString()が指定されていない場合それを推測しようとします。文字列の表現として使うために、これはタイトル、名前、題目、などの名前を持つカラムをチェックします。

tip

sfWidgetFormDoctrineSelectウィジェットのmethodオプションはオブジェクトをテキスト形式で表現するために使われるメソッドを変更します。

図4-4は__toString()メソッドを実装した後で記事を作る方法を示します。

図4-4 - 記事を作る

記事を作る

生成フォームをカスタマイズする

doctrine:build-formsdoctrine: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));
  }
}

ここではvalidatorSchemawidgetSchemaオブジェクトをPHP配列として使います。これらの配列はフィールドの名前をキーとして取り、バリデータオブジェクトとウィジェットの関連オブジェクトをそれぞれ返します。フィールドとウィジェットを個別にカスタマイズできます。

note

オブジェクトをPHP配列として使うために、sfValidatorSchemasfWidgetFormSchemaクラスが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つのテーブル間の一対多のリレーションを表すように、sfWidgetDoctrineSelectManysfValidatorDoctrineChoiceManyバリデータは多対多のリレーションを表し同じオプションを受け取ります。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_atupdated_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はカスタマイズするArticleFormAuthorFormフォームを示します。

リスト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()メソッドが国際化テーブルも自動的に更新することを見ることになります。

sidebar

bindAndSave()メソッドを使う

bindAndSave()メソッドはユーザーがフォームに投稿した入力データをバインドし、このフォームをバリデートしてデータベースの中の関連オブジェクトを更新します。すべてが1つのオペレーションで行われます:

class articleActions extends sfActions
{
  public function executeCreate(sfWebRequest $request)
  {
    $this->form = new ArticleForm();
 
    if ($request->isMethod('post') && $this->form->bindAndSave($request->getParameter('article')))
    {
      $this->redirect('article/created');
    }
  }
}

ファイルのアップロードを扱う

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タスクによって初期に生成されたコードとまったく同じです。

sidebar

フォームのモデルのコードをリファクタリングする

doctrine:generate-crudタスクによって生成されたアクションは通常は修正すべきではありません。

editアクションに追加できるアクションは、とりわけフォームのシリアライズの間に、通常はモデルクラスもしくはフォームクラスに移動させなければなりません。

アップロードしたファイルを保存する方法を考えるためにフォームクラスのリファクタリングの例を先ほど見直しました。モデルに関連する別の例を考えてみましょう。ArticleFormフォームはslugフィールドを持ちます。ユーザーによって上書きされる潜在的な可能性があるtitleフィールドの名前からこのフィールドは自動的に算出されることを見ました。このロジックはフォームに依存しません。それゆえ次のコードで示されるように、フォームはモデルに所属します:

class Article extends BaseArticle
{
  public function save($con = null)
  {
    if (!$this->getSlug())
    {
      $this->setSlugFromTitle();
    }
 
    return parent::save($con);
  }
 
  protected function setSlugFromTitle()
  {
    // ...
  }
}

これらのリファクタリングの主な目的はアプリケーションレベルでの分離、そしてとりわけ開発の再利用性を尊重することです。

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()メソッドによって呼び出されます。